Redis 笔记
Redis 中常见的数据类型有哪些?
基础的数据类型就是:String,List,Hash,Set,ZSet
- String:经典数据类型,存储文本,数字,二进制,单个最大为 512 MB,缓存用户会话,计数器都可用
- List:有序的,字符串列表,底层是双端队列,可以用于消息队列场景
- Hash:键值对集合,适合存储热点对象属性,类似于商品信息之类的数据可以使用
- Set:无序,不重复的集合,适合做需要去重的场景
- Zset:和 Set 差不多,区别是带有一个 分数 元素来排序,可以实现排行榜的操作
除此之外还有高级数据类型
- BitMap:在 2.2 版本引入,用来存储位数据,每个 bit 表示一个哈希槽,只存 01,适用于存储用户是否签到之类的。用 SETBIT 设置状态,GETBIT 读取状态:
- HyperLogLog:2.8 版本引入,是概率性的数据结构,用来估算基数的,无论多少数据,只占用 12 kB 的内存,缺点是不是百分百准确的,适合统计 UV 这种精度要求不高的场景
- GEO: 地理信息,支持经纬度存储和空间查询,底层是用 ZSet 实现的,可以用来查询 附近的人 的操作
- Stream: 5.0 版本引入,专门用于消息队列,支持了消费组的设定,还支持唯一消费 ID,还可以持久化,即使消费者挂了重启也可以重新消费
Redis 为什么这么快?
- 数据保存在内存中,除了持久化,基本不和硬盘打交道,内存比硬盘快太多了
- 单线程 + IO 多路复用:单线程意味着不会发生抢占,也不会因为上下文切换产生性能损耗,网络 IO 使用 epoll 多路复用,一个线程处理几万个 socket 链接,哪个链接有数据就处理哪个
- 高效的数据结构,Redis 每个数据结构都经过精挑细选,String 底层使用 SDS,Hash 数据小的时候使用 zipList,数据大的时候使用 HashTable 保证查询 O(1),ZSet 使用跳表,插入,查询都是 O(Log N)
为什么 Redis 设计为单线程?6.0 版本为何引入多线程?
单线程意味着 Redis 的性能瓶颈不在 CPU,而是网络和内存
单线程的优势很明显,代码简单,不容易出 bug,无线程间竞争,没有锁和上下文切换的开销,一个线程盯着十几万个 socket,单机就能处理 十万+ 的QPS,大多数场景都够了
至于 6.0 为什么引入多线程,现代业务并发需求大,瓶颈出现在网络 IO 了,解析 Socket 的数据成了压力,所以就引入了 Threads 来处理网络 IO,指令还是有由一条主线程发送处理的,
另外 4.0 引入了后台线程,处理大 Key 的删除,AOF 刷盘这些耗时操作,防止主线程阻塞
Redis 在生成 RDB 文件时如何处理请求?
RDB 产生自主线程发送的 bgsave 由fork出的子线程执行,主线程会将内存页表复制给子线程一份,子线程通过这个复制品来制作 RDB,写入磁盘,主线程仍然处理用户请求,增量使用写时复制技术来保证子进程数据不变
生成 RDB 文件的时候如何保证一致性?
主线程在 fork 子线程的时候并不是去复制一份完整的内存数据给子线程,这样太浪费空间而且太慢,主线程会复制一份页表给子线程,子线程通过这个页表去遍历内存,还是共享一块内存
但是这样,主线程改,子线程读,还是会被污染,Redis 采用了写时复制技术,如果有写入操作,就复制一份当前内存页的副本出来,主线程在副本上更改,子线程去读旧的内存页,这样效率高,而且也能保证数据就是 fork 那一瞬间的数据,不会被后续操作污染
高峰期使用 RDB 有什么问题?如何优化?
- 首先 RDB 期间,主线程采用写时复制进行写入,极端情况下会造成内存翻倍占用
- 子线程需要将 RDB 文件写入磁盘,如果缓存数据集很大,写入磁盘的时间相当客观,坏情况数分钟不止,磁盘IO跑满,会影响其他的AOF 和其他业务
- fork 子线程的时候主线程也会阻塞,虽然是复制的内存页表,但是在数据量很大的情况下,内存页的大小也相当可观,阻塞时间可能得几十毫秒甚至上百毫秒,这个时候,主线程是不能去工作的
所以一般会去设置 RDB 的时间为低峰时段跑,或者用从节点跑RDB,不耽误主节点处理请求
fork 的时候为什么会阻塞主进程?不是说只复制页表吗?
复制页表也是需要时间的,fork 期间会被阻塞,Redis 使用虚拟内存,每个内存页通常有 4kb,如果一个有 20G 的实例去做 RDB,主线程需要去复制一个差不多几十MB的页表,差不多需要消耗100毫秒了
如果 RDB 生成到一半机器挂了怎么办?
挂了就挂了,并不会造成很大的损失,RDB 操作是先写一份临时的 RDB 文件,然后再由 rename 操作转正为 dump.rdb,rename 是原子性的,如果失败了,就还是去恢复上一次的 RDB 文件。
bgsave 和 save 有什么区别?生产上用哪个?
save 是同步操作,主线程去做备份,期间完全阻塞,bgsave 是交给子线程的,异步操作,生产要用 bgsave,主线程阻塞带来的服务关停是不可承受的
Redis 中跳表的实现原理是什么?
跳表是一种多层链表,底层的链表存储数据,按照一定规则排序,上层的链表负责索引,上层是下层的子集,跳表相对于链表可以将查询操作优化到 O(N)
查找过程:首先从最上层的链表开始,往左找,如果左侧节点比查找值大,就下沉到下一层节点,然后一直循环到最底层找到数据或者数据不存在为止。
插入过程:首先通过查找的方法定位到数据层的插入位置,然后通过随机算法来确定要增设几层索引,Redis 使用百分之二十五的概率增加一层
删除过程:也是先找到节点,然后在每一层更改前后的引用,就跟普通的链表一样
Redis 的小巧思:在最底层的链表增设了回退指针,因为 ZSet 提供了反向查询的命令,可以倒序查数据
Redis 的小巧思:随机索引层的概率算法,Redis使用了随机数来确定当前插入的数据应该建立几层的索引,具体来说,Redis 规定了最多只能存在 32 层索引,然后在一个循环中取这随机数,看看是否小于32 的 百分之二十五,如果是的话就层数+1,不是就退出,最大是 32,通过最后计算出的层数来决定增加几层索引
Redis 的 hash 是什么?
Hash 是键值对的集合,可以将一堆键值对放在一个 key 下面,可以用于存储对象数据,每个字段相互独立,不像 Json 得全部覆盖,编码使用两种数据结构:总大小不超过 512 字节或者单个字段不超过 64 字节的状态使用 listpack,反之使用 hashtable 存储,查询效率为 O(1)
其实在 Redis 7 之前,Hash 不是使用的 Listpack,而是 ZIPList,但是 ZIPList 虽然也是紧凑型的数据结构,但是他维护了一个前一个节点长度的数据,这会导致级联更新问题,更新一个数据,就有可能要同步更新后面所有的数据,listpack 只存储当前节点的长度,避免了这个问题
Redis Zset 的实现原理是什么?
ZSet 的底层是跳表 + 哈希表配合实现的,跳表使用 score 进行排序,支持 O(Log N)的插入,删除,范围查询,哈希表存储 member 到 score 的映射,用来保证 O(1) 的查询
显而易见,两者各有所长,哈希表保证了 O(1) 的快速查询,效率极高,跳表保证了高效的范围查询,Redis 的跳表数据层是双向链表,支持正向 / 负向的范围查询
更新数据的时候,需要同时维护这两张表,同时更新其数据结构
当元素比较少的时候(总元素少于 128 个,且,每个元素不超过 64 字节)使用 Listpack 来存储,反之使用 跳表 + 哈希表
Redis 中如何保证缓存与数据库的数据一致性?
数据源同步问题没有很好的解决方案,只能说是有取舍
- 先更新缓存,再更新数据库:这种方法很容易出问题,两个请求同时更新,A 先更新缓存,B更新缓存,B 更新数据库,A 更新数据库,就会出现缓存和数据库不一致的情况
- 先更新数据库,再更新缓存:和前者一样,由于执行顺序不同,会出现缓存和数据库不一致的问题
- 先删缓存,再更新数据库:也很常见,A 删除了缓存,B 来查询,发现查不到,去数据库拿了旧数据,A 更新缓存,B 更新缓存
- 先更新数据库,再删缓存:缺点就是有很短的时间数据可能不一致
- 缓存双删:先删除缓存,然后写数据库,然后延迟后再删除缓存,可以避免多任务写入旧数据带来的不一致问题
- Binlog 异步更新:Canal 监听 MYSQL Binlog,通过消息异步同步到缓存,一致性最好,也不用操心缓存,自动更新,就是要做幂等这种操作而已
如果实在是不能容忍任何的不一致,比如金融场景,就只能进行加锁操作了,读写互斥,读操作不互斥
Redis 中的缓存穿透、缓存击穿和缓存雪崩是什么?
缓存穿透:大量请求访问不存在于缓存中的值,请求打到数据库,这种常见于攻击行为,攻击者随便找一个不存在的 ID,进行查询,消耗数据库资源
- 对请求的参数进行合法校验,防止拿着错误的参数进行查询
- 使用布隆过滤器,加载需要的 Key,进行过滤操作,不存在就是不存在
- 如果数据库查询之后,就在 Redis 中放置空值,设置过期时间,防止大量数据打入
缓存击穿:某个热点 key 过期之后,大量请求被打到数据库,数据库压力暴增
- 过期之后,可以只让一个任务去数据库查询,其他的等待更新就可以
- 还可以在后台起一个线程用来专门刷新热点数据
缓存雪崩:大量热点 key 在同一时间过期,或者 Redis 直接就是挂了,请求全部走到了数据库,直接爆炸
- 如果是集体过期,就设置浮动值来避免大量 Key 直接过期
- 如果是 Redis 崩了,就要考虑架构的问题了,可以设置 Redis 集群,主从 + 哨兵的机制让 Redis 即使挂了一个节点也没问题,也可以使用 Guava Cache 做一层本地缓存,先顶一会,看看 Redis 能不能重启成功,或者也可以直接熔断,防止产生雪崩
Redis String 类型的底层实现是什么?(SDS)
SDS 是 Redis 对 C++ char 数组进行的改进,原来的 char[] 数组获得长度是 O(n) 的时间复杂度,并且自动检测 \n 字符,而且扩容还得自己搞,
SDS 维护了除了数据区 buf 以外,还维护两个额外的区域:len 和 alloc,前者用于记录数据长度,后者用于记录分配区域大小,获取长度操作简化为了 O(1),插入时使用 alloc - len 进行空间检测,不够就扩容,buf 末尾保留 \n 用于兼容 C
另外,其还采用了不同的编码方式应对不同的状况,
- 如果能解析成整数,直接使用 int 编码。直接放在指针位置,不用 SDS
- 如果字符串长度小于 44,使用 embstr,redisObject 和 SDS 放到一起,一次 malloc 就可以
- 超过 44 的话,使用 raw 编码,分开存储
至于为什么是 44,Redis 的内存分配器以 64 字节为一个分配单元,RedisObject 占用 16 字节,sdshdr8 头部 3 字节,末尾 \0 一字节,一共 二十字节,剩下 44 字节
Redis 中如何实现分布式锁?
其实很简单,Redis 使用 SET key value EX seconds NX 就可以进行创建锁,解锁可以使用 Lua 脚本校验删除,
加锁的时候先检测这个 key 是否存在,如果存在就不设置,说明拿不到锁,返回失败,如果不存在,就设置成自己的唯一识别码,然后返回成功
解锁的时候使用 lua 脚本先获取锁的 Value,检查是否是自己的唯一识别码,如果是的话就解锁,返回成功,如果不是的话就返回失败,
注意,分布式锁必须设有过期时间,可以防止拿到锁的服务挂掉,锁永不释放的情况出现
Lua 脚本可以让检查 Value 和删除的过程绑定为原子操作,防止分开执行中间被人插一脚
唯一的问题是单点下,Redis 挂了,锁就没了,这个对生产环境产生的打击是致命的,主从也容易出现不一致的情况,解决方案就是 RedLock,
RedLock 就是让多个 Redis 通过多数成功的机制加锁,解决了分布试下锁容易随着主节点崩溃而消失的问题,缺点就是需要多个节点同步,效率低
Redis 的 Red Lock 是什么?你了解吗?
红锁 - RedLock,Redis 使用的一种分布式锁,用于解决分布式环境下,Redis 集群锁丢失的问题
普通的 Redis 做分布式锁有丢失的风险,就是主节点挂了之后,从节点没来得及同步锁的消息,就直接晋升成为主节点了,锁就消失了
RedLock 采用 多数投票机制,拿到大多数节点锁的操作才算成功,所以即使主节点爆炸,从节点也能保证带有锁
RedLock 加锁流程:
- 客户端记录当前的 时间戳 T1
- 依次向多个 Redis 实例发送请求,请求授权锁,如果这个请求超时就跳过
- 最后轮训完一遍,拿到这个时候的时间戳 T2
- 统计成功的请求数目,统计请求成功的节点数,统计总耗时 T3 = T2 - T1
- 如果通过的实例数 >= 总实例数 / 2 向上取整,并且 T3 < 锁过期时间就说明加锁成功
- 失败就向所有实例发送解锁请求,表示加锁失败,撤了吧
其实这玩意也不是绝对安全的,首先就是:
- 线程1请求到了锁,然后发生 GC了,进程暂停,锁过期
- 线程2拿到了锁,美美执行任务
- 线程1复活,美美执行任务
- 两个线程美美共用同一个临界区的资源
除了这个,由于每个锁都是在本地执行的,发生时钟跳变也会影响锁的唯一性
一般情况下,Redisson 的看门狗就能解决这个问题
Redis 实现分布式锁时可能遇到的问题有哪些?
如果使用简单的锁的话遇到的问题也不少:
- 锁在业务执行前自动过期,数据自然就乱了,可以使用续约机制,到期之前检测业务是否正常进行,如果是的话,直接续约
- 线程发生 CG,锁过期,其他线程持有了锁,线程醒过来又给删了,解决方案一般是设置 唯一设别码,用来判断是不是自己的锁
- 如果单点 Redis 爆炸,锁就爆了,如果主从同步,主节点爆了,从节点还没有同步到,锁也会丢失,可以使用 RedLock
- 锁不可重入,解决办法是设置重入次数,重入一次 +1,释放一次 -1
Redis 的持久化机制有哪些?
主要有两种持久化的机制:RDB 和 AOF
RDB:快照持久化技术,通过记录这一瞬间的数据快照然后通过 dump 保存内存中的数据,优点是加载速度快,数据直接加载到内存中,缺点就是备份间隙的数据容易丢,比如五分钟一次备份,最多就丢五分钟数据
AOF:日志持久化技术,将指令追加到 AOF 日志中,安全性最高,每条指令都备份,缺点就是一直备份,备份文件体积大,恢复的时候需要全量执行指令,速度慢
两者各有优缺点,可以结合起来,4.0 引入了混合持久化,AOF 重写的时候先用 RDB 保存快照,快照间隙产生的增量用 AOF 追加到文件末尾。兼顾恢复速度和安全性
说说 AOF?
AOF 是Redis 提供的一种日志式的持久化操作,通过维护一个 AOF 文件,将每条指令都追加到 AOF 文件的末尾,因此,这玩意也比 RDB 安全,最多丢掉一秒钟的指令。但是由于每条指令都追加,这玩意的文件大小也大,恢复速度也慢,
由于 AOF 日志存放在磁盘中,如果每次指令都需要写回磁盘的话,压力太大,Redis 提供了好几种方案用于 AOF 日志的写回
- always;每条指令都追加到 AOF 中,最安全,性能最低,因为要等待磁盘 IO
- everysec:每秒刷一次,时间和效率的妥协,默认就是这个,毕竟,一秒能存什么牛逼的数据呢,对吧?
- no:不主动存,让操作系统判断什么时候该存,性能最好,但是最容易丢
即使是 always 模式下,也容易丢,因为 Redis 执行指令和 AOF 写回并不是一个原子操作,如果中间断了,就直接丢了,所以 Redis 并不能做到完全不丢数据,毕竟性能才是 Redis 的卖点
AOF 由于一直追加会导致日志文件爆炸大,而且一条数据被 Set 一千次,只有最后一条是有用的,这就有了 AOF 重写机制: 重写操作进行的时候 fork 出子进程,子进程根据内存快照重新制定一份 AOF 文件,在这中间产生的增量同时追加到旧 AOF 和 重写缓冲区,然后子进程追加重写缓冲区的数据到新 AOF,然后用新 AOF 替换掉旧 AOF
重写可以通过自动触发也可以手动
Redis 主从复制的实现原理是什么?
主从复制就是 Redis 集群用于同步数据的方案,主要是主节点向子节点同步数据,增加负载
主要分为三步:
- 建立全量同步:子节点链接的时候,找到主节点,发送一个 PSYNC 命令,表示自己第一次来,想要跟着大哥混,索要数据,主节点就会进行一次全量同步,主节点会 fork 一个子线程用于生成一份 RDB 快照发送给从节点,从节点卸载掉自己的旧数据,加载得到的 RDB,在这个过程中,主节点并不会停止服务,这个过程中产生的增量会保存到 Replication Buffer 中,从节点同步完成之后,再发送这个缓冲区的数据,保证数据不丢
- 数据同步:主节点每收到一个命令之后,就会异步发送给从节点,从节点执行这些命令并且互相发送心跳保证存货
- 重连恢复:如果子节点挂了,重连的话,全量同步太费时间了,而且消耗资源,所以就有了增量同步这一说,Redis 在 2.8 之后维护了一个 repl_backlog_buffer 用于存储近期数据,这个缓冲区是环形存储的,存储满了就从最老的节点进行覆盖,子节点重连之后,会发送自己的偏移量,主节点检测是否存在这个数据,如果存在就同步,不存在就全量
Redis 数据过期后的删除策略是什么?
Redis 提供了两种删除方法
- 惰性删除,过期的 Key 不会马上被清除出去,而是会在被读取的时候顺手检测一下,如果过期就删掉并且返回空,好处就是不会占用太多 CPU 去扫描,坏处就是冷数据不会走
- 定期删除其实也不是完全删除,而是进行抽样检测,发现过期就清理,避免 CPU 全在干清理的活
如何解决 Redis 中的热点 key 问题?
热 Key 就是被高频访问的 Key,比如大促热门商品等,一秒几十万的请求打到一个 Key 上面,Redis 单线程处理命令,大量的请求可能直接占满 CPU 打掉 Redis,主要解决思路就是分散压力
- 可以在 Redis 前面加一层本地缓存,比如 Cache,让 JVM 直接承担热 Key 的响应,如果能命中百分之九十的热 Key,Redis 压力直接少一个数量级
- 读写分离:Redis 提供了主从节点来改善这种情况,从节点负责读取,这样就不会都打到一个 Redis 了
- 限流:在网关层限制对热 Key 的访问,超过阈值直接快速失败,与其让用户看到 活动过于火爆,也不能让 Redis 崩溃造成雪崩
关于这个 热点Key,如果一个 8 核的 Redis 实例,整体的 QPS 能到十万,如果一个 key 的 QPS 到了五千就可以准备报警了,超过一万就说明是热点 key 了
Redis 集群的实现原理是什么?
Redis 集群的设计目的是解决单机瓶颈问题,将数据分成不同的块,节点之间通过 gossip 协议通讯,每个 Redis 负责不同的块,降低单机压力
首先,Redis 将所有的数据分成了 16384 个槽位,每个节点负责一部分,也就是哈希槽,数据过来之后,计算出 CRC16 值,然后对着 16384 取模,就可以知道这个归谁管了,直接送到对应的 Redis 中,好处就是增加 Redis 节点之后,只需要重新分管哈希槽就可以知道数据存在什么节点了,而不需要重新计算哈希值。因为槽位是固定的
节点之间通讯使用 gossip 协议通信,大概来说,每个节点随机选择几个节点,和这些节点进行信息交换,主要有分管哈希槽的信息,心跳等,这样去中心化也能保证在很短的时间内节点的一致性,也能知道整体的拓扑结构
至于客户端如何获取集群的数据,这个就很简单了,客户端首先随便链接集群中的一个节点,然后去拿着 key 找数据,如果恰巧这个 key 所在的哈希槽就归这个 Redis 分管,那么就可以直接获取数据,如果不在,Redis 会查看这个哈希槽归谁管,然后返回这个节点的连接信息
Redis 中的 Big Key 问题是什么?如何解决?
Big key 就是指比较大的 key,会产生问题:
- 大 key 占用空间很大,集群模式下,哈希桶是平均分配的,大 key 可能集中在某一个实例中,影响查询效率
- Redis 是单线程处理的,处理大 key 消耗的资源可能会影响其他任务的进行,比如内存资源,网络资源,可能会导致其他任务的超时
解决大 key:
- 可以改变存储方式,可以拆分大 key,或者可以对数据做压缩,Json 数据可以用 Hash 存储
- 尽量只存储必要数据,从业务层减小 key 的体积
- 使用 Redis Cluster 部署,减少单片压力
Redis 通常应用于哪些场景?
- 缓存:Redis 将数据存放在内存之中,速度很快,天然适合做缓存,用于将一些热点数据存储在 Redis 中,防止所有的请求打到数据库,提升系统性能
- 分布式锁:Redis 指令都是由一个线程完成的,天然保持原子性,适合做锁
- 高频计数器:Redis 基于内存,性能高,配合 incr 做自增,不会算错
- 排序业务:Redis 自带一个 ZSet 的数据结构,天然排序,速度很快,适合做实时大榜之类的场景
- 轻量消息队列:如果不想用 kafka 这种重型消息队列,可以使用 Redis 自带的 List 来实现
如何使用 Redis 快速实现排行榜?
Redis 提供 Sorted Set,底层使用跳表实现,插入,删除,查询的时间复杂度都是 O(logN),Sorted Set 根据 score 进行排序
Sorted Set 支持范围查询,查询某个用户位与多少名,加分,减分都可以
如果 score 相同,就按照 member 的字典序排序,如果想按照时间排序,就可以将时间戳写入到 score 中,这样就可以根据时间排序了
百万级的排名可以,但是再往上压力就大了,可以进行分片:
- 比如可以按时间进行分片,这种可以针对多个排行榜的场景,比如月榜,日榜之类的
- 可以根据分数的大小进行分片。比如 0~1000 分一个榜,1001 ~ 2000 一个榜,在业务层进行聚合
- 对用户取模进行分片,适合不关心全局,只关心附近的场景
如果排行榜需要支持实时更新,怎么保证一致性?
Redis ZADD 本身就是原子操作,不用考虑单条数据的并发问题,如果是多个用户去更新,可以用事务或者脚本的方式进行操作,也能保证原子性,如果是分布式场景,使用分布式锁也能保证避免覆盖问题
排行榜数据量特别大,比如上亿用户,怎么优化?
将排行榜进行分层,活跃的用户放到 Redis,不活跃的放到库中,查询的时候先查缓存,没查到再查库
也可以做分片,将用户按照 ID 进行分片存储,查询的时候再进行聚合
分数相同怎么处理?比如游戏里先达到的应该排前面
分数相同默认按照字典序进行排序,如果想要按照时间的话,可以将时间写到分数后面,这样就会按照时间进行排序了
为什么 Redis Zset 用跳表实现而不是红黑树?B+树?
- 跳表实现简单,本质就是几层链表叠到一起,没有红黑树的左旋右旋,没有 B+ 树的分裂合并
- 范围查询简单有效,跳表底层就是完整的数据,想要范围查询直接定位到最底层然后遍历就行,红黑树还得中序遍历,很麻烦,B+ 倒是可以
- B+ 树设计是为了减少磁盘 IO 设计的,矮胖的设计可以减少巡道次数,适合将数据存放到硬盘的类型,Redis 数据在内存,用不上这个
Redisson 看门狗(watch dog)机制了解吗?
看门狗是为了解决 Redis 分布式锁的一个常见问题:业务申请锁,正在执行,但是锁过期了,业务还在跑,别的线程进来获取锁,一同进入临界区,这个时候业务就乱了
原理很简单,当业务去申请锁,但是没有设置 leaseTime 的时候,Redisson 就会自动开启一个定时任务,没过十秒就检测一遍业务是不是还在跑,如果是的话就向 Redis 发送一个续约的请求,将锁的超时时间重新设定成 30 秒,这样就可以防止业务没跑完,锁却到期的问题
锁被释放的时候,定时任务会取消,如果客户端宕机,定时任务也会一起挂掉,所以并不会出现无限续约的情况,三十秒后就自动释放了
最后更新时间:2026/4/10