一文掌握Redis常见八股 数据类型String可存储字符串整数或浮点数计数器/分布式锁底层实现SDS简单动态字符串Hash可存储多个键值对缓存对象底层listpackList可重复插入取出有序朋友圈点赞。评论列表底层QuickList快速列表一种双向链表每个节点是一个压缩列表set不可重复去重Zset有序集合自动按分数排序元素排行榜BitMap存储⼆进制位布隆过滤器用户签到记录Stream消息队列重点讲一下跳表SkipList跳表本质上是一个多层链表底层链表保存所有元素上层链表是下层的子集。通过这种分层索引结构把链表的 O(n) 查找优化到 O(log n)。查找查找的时候从最高层开始先往右走遇到比目标值大的就往下一层。重复这个过程直到找到目标或者确定不存在。插入先用查找的方式定位到插入位置然后随机决定新节点要建几层索引。Redis用25%的概率往上加一层所以大部分节点只在底层。删除跟普通列表删除一样。分布式锁基于Redis的setnx命令Setnx如果 key 存在就返回 0如果 key 不存在就返回 1。实现第一步加锁使用set unique key NX ex TTL。A. 加锁成功执行命令结果返回 1。B. 加锁失败执行命令结果返回 0。C. 能实现互斥且设置了 TTL可以通过 TTL 到期自动删除 key自动释放锁防止死锁。判断锁是否是本线程加上的是为了防止锁误删的情况。举例线程 1 业务阻塞锁的 TTL 到期Redis key 自动删除则锁释放。线程 2 获取到了锁。在线程 2 正常执行时线程 1 恢复运行。如果线程 1 不判断当前锁是否是自己加上的直接把锁删除了就出现了并发问题。第二步释放锁。执行 delete 删除 key 的命令。A 先 get key 拿到 value判断当前锁是否是本机器本线程加上的如果是正常删除如果不是则不需要处理。B 用 Lua 脚本来完成“判断是否是当前线程的锁”和“删除 key 释放锁”这两个操作保证原子性。判断锁归属和删除 key 这两个操作应是原子操作。举例 当线程 1 判断完是自己的锁准备去删除时因为某个原因导致线程 1阻塞Redis 锁的 TTL 到期锁被迫释放。随后线程 2 获得了锁。此时线程 1 恢复执行去删除锁就会出现误删。问题由于业务线程执行耗时太长TTL 到期锁被迫释放存在多个线程并行的情况。setnx 线程不可重入。TTL 不好设置业务到底要执行多长时间不确定。基于Redisson相较于setnx命令实现的分布式锁更加强大和完善。Redisson可重入锁原理可重入锁即一个线程可以多次获取锁利用哈希结构记录获取锁的线程和获取锁的次数。获取锁的逻辑 A判断 key 是否存在即判断是否有线程持有锁(a) 若不存在代表没有线程持有锁则获取锁即设置一个哈希结构field 是机器码加线程value 是次数 1。(b) 若存在代表有线程持有锁此时判断持有锁的线程是否是当前线程。如果是当前线程则重入次数加 1。释放锁的逻辑(a) 重入次数减 1。(b) 如果重入次数为 0删除 key 释放锁。Redisson分布式锁的数据未同步导致线程安全主从问题主节点宕机时若从节点未同步 锁 数据 → 新主节点可能允许其他线程重复加锁 → 锁失效。解决部署多个 Redis 主节点采用多主多从架构向多个主节点都去获取锁。连锁方案要求所有主节点加锁成功否则立即失败并释放已获取的锁。红锁方案要求半数主节点以上加锁成功否则立即失败并释放已获得的锁。持久化在Redis的默认配置文件redis.conf中RDB快照是默认开启的而AOF日志需要手动配置开启。RDBRDB把内存中的所有数据都记录到磁盘文件中。当 Redis 实例故障重启后读取磁盘快照文件恢复数据。RDB触发机制手动执行命令save 命令由 Redis 主进程来执行RDB 会阻塞所有命令。BGSAVE 命令开启子进程执行 RDB避免主进程受到影响。save 900 1 # 900秒内如果⾄少有1个key被修改则执⾏ bgsave 命令 save 300 10 #300秒内如果⾄少有10个key被修改则执⾏ bgsave 命令 save 60 10000 #60秒内如果⾄少有10000个key被修改则执⾏ bgsave 命令RDB异步持久化的底层原理异步持久化即 bgsave就是开启一个子进程由子进程读取内存数据并写入 RDB 文件。BGC 对主进程几乎是 0 阻塞的但有一个过程会阻塞主进程那就是 BGC 刚开始时fork 主进程得到子进程(主要是把⻚表复制给⼦进程)子进程共享主进程的内存数据。这个 fork 的过程是阻塞的Redis 在 fork 中只能做这一件事不能去接受用户请求。进程在执行 bgsave 时读取共享内存的数据主进程如果在写内存数据可能会造成冲突。因此为了避免这个问题底层会使用一种 copy on write 的技术内存数据会被标记为 read only。当主进程执行写操作时则会拷贝一份数据在拷贝的数据上执行写操作。优缺点优点宕机后恢复速度快文件体积小。缺点数据安全有问题因为 RDB 执行间隔时间长两次之间写入的数据有丢失风险。但是 RDB 的间隔时间又不能设置太短因为 RDB 的过程十分耗时如果间隔时间短根本忙不过来。fork 子进程复制页表、写出 RDB 文件等都比较耗时。AOFAOF 全称为 Append Only File追加文件。Redis 处理的每一个写命令都会记录在 aof 文件中所以可以把 aof 文件看作命令日志文件。AOF 执行的频率默认是每秒钟做一次。always同步刷盘。写日志文件的操作是由主进程在写入内存数据后完成的性能影响大。everysec每秒刷盘。写内存后先把命令放入 AOF 缓冲区然后每隔一秒将缓冲区数据写入 AOF 文件。no操作系统控制。写内存后把命令放 AOF 缓冲区由操作系统决定何时将缓冲区内容写入 AOF 文件。AOF文件重写因为是记录命令AOF 文件会比 RDB 大得多而且 AOF 会记录对同一个 key 的多次写操作但只有最后一次写操作才有意义。通过执行命令可以让 AOF 文件执行重写功能用最少的命令达到相同的效果但这个过程会占用大量资源。优点数据安全性更高例如 everysecond 策略只会丢失一秒以内的数据。缺点宕机后恢复速度慢因为 AOF 文件记录的是命令需要依次执行。文件体积大需要进行 AOF 文件重写此时会占用大量资源。混合持久化Aof和RDB各有优缺混合持久化同时拥有上述两种持久化的优点。开启混合持久化后当aof文件重写时将当前内存数据以RDB格式写入新aof文件的开头。后续增量数据以aof格式追加到文件末尾。混合持久化结合了 RDB 和 AOF 持久化的优点开头为 RDB 的格式使 Redis 可以更快地启动同时结合 AOF 的优点降低数据丢失风险提高数据安全性。内存淘汰策略一类是针对TTLlru淘汰最久未使用的key。LFU淘汰访问频率最低的 key。Random随机选择一个 key 删除。TTL选择剩余时间最短的 key 删除。一类是针对所有key进行淘汰LRU从所有 key 中淘汰最久未使用的 key。LFU从所有 key 中淘汰访问频率最低的 key。Random从所有 key 中随机删除一个 key。淘汰流程客户端执⾏写⼊命令触发内存检查Redis 检查当前内存使⽤是否已超出 maxmemory(配置⽂件设置的值)根据配置的策略选择待淘汰键删除键并触发相关事件如 evicted 通知缓存击穿/穿透/雪崩缓存击穿通常发生在高并发场景下某个热点数据在缓存中过期大量请求打到数据库导致数据库压力过大造成崩溃。解决方案互斥锁思路只允许一个线程去查询数据库并重建缓存其他线程阻塞等待。这个方案可以避免大量请求都想重建缓存给数据库带来压力。“永不过期”逻辑过期缓存 key 不设置 TTL实际上在 value 中多存储一个属性——过期时间。缓存穿透数据在Redis缓存和数据库中都不存在。这样缓存永远不会生效。请求直接打到数据。解决方法缓存空值布隆过滤器插入数据时通过 N 个哈希函数依次对 key 进行计算得到数据 key 的特征。key 的特征就是一长串二进制串其中那些 1 就是特征。 通过布隆过滤器判断数据是否存在时再用 N 个哈希函数对 key 进行计算分别判断二进制串上对应位置的 0 或 1 是否是 1。如果都是 1则认为数据存在否则数据一定不存在。缓存雪崩大量key同时过期或缓存服务宕机大量请求打到数据库导致数据库瞬时压力过大甚至崩溃。解决方案均匀设置过期时间例如加随机数Redis集群提⾼服务可⽤性热Key问题时间内被高频访问的key。影响导致 Redis 实例的负载激增可能引发性能瓶颈或服务不可用。甚至在 key 过期时可能导致缓存击穿。解决3. 本地缓存使用 Java 的 Caffeine 本地缓存减少 Redis 的压力注意设置过期时间。4. Key 分片将热 key 拆分成多个子 key分散到不同的 Redis 实例上。5. 读写分离读请求分流到多个从节点。大Key问题定义⼤Key是指 Value体积过⼤如String类型 2MBHash/List元素5000个的Key影响对大 key 的操作导致主线程的操作阻塞和资源消耗过高。若使用 RDB 持久化可能内存峰值升高导致 OOM。解决数据超分哈希 list 拆分将大 key 拆分成多个小 key。异步删除命令使用 unlink 代替 delete由后台异步线程删除避免影响主线程。Redis高可用Redis主从主从分离主节点负责写从节点负责读Redis哨兵作用实现主从集群的自动故障恢复。监控哨兵会不断地检查 master 和 slave 是否正常工作。自动故障恢复如果 master 故障哨兵会将一个 slave 提升为 master同时哨兵会去重启 master 尝试让其恢复。通知哨兵充当 Redis 客户端的服务发现来源。当集群发生故障转移时会将新消息推送给 Redis 客户端通知的信息包括主节点信息、从节点信息。Redis 客户端收到信息才能做读写分离。Redis 集群分片集群的特征集群有多个 master每个 master 保存不同数据。每个 master 都可以有多个 slave 节点并且之间存在主从数据同步。数据监测和自动恢复集群之间通过心跳机制互相监测彼此的健康状态。主节点之间互相心跳监测主节点定期向它的从节点发送 ping 消息并等待 pong 响应以确认它所管理的从节点是否正常。客户端如何访问客户端可以请求任意节点最终都会被转发到正确节点。数据分片-散列插槽的原理Redis 会有 16384 个插槽分别分配给不同的 master 节点存入 Redis 的数据。key 是与插槽绑定的而不是与从节点绑定。Redis 会根据 key 的有效部分计算插槽值以此判断 key 是和哪个插槽绑定的。计算插槽值使用CRC16算法根据key有效部分计算出hash值然后对16384取余达到slot插槽值为什么将数据与插槽绑定来实现分布式存储5. 负载均衡当集群伸缩时Redis 集群将哈希槽重新分配保证数据和负载的均衡分布。6. 快速查找通过将数据与插槽绑定可以计算 key 对应的插槽值快速定位到数据所在的 Redis 节点避免全局查询。并且即使集群伸缩或故障恢复后都可以根据插槽值找到数据所在的新 Redis 节点。Redis集群的优点7. 实现了数据分片通过多 master 和散列插槽的机制允许承载更大的数据量。8. 实现了故障检测和自动恢复通过心跳机制监测集群节点健康状态。9. 支持集群伸缩可新增 master 节点及其 slave 节点可应对更高的读写需求。总结Redis 主从是一主多从架构Redis 哨兵是一主多从加哨兵集群架构Redis 集群实现的是一个多主多从架构。主从数据同步原理与实践优化全量同步原理主从建立连接后第一次数据同步即为全量同步。流程Slave 请求数据同步Master 判断是否为第一次同步。Master 判断是第一次同步返回数据版本信息。Master 执行 BG save 生成 RDB 文件并且在生成文件期间将主进程收到的所有命令记录到内存缓冲区。Master 向 Slave 发送 RDB 文件Slave 清空本地数据并加载 RDB 文件。Master 向 Slave 发送内存缓冲区中的命令Slave 收到命令后执行。Master如何判断slave节点是否第一次来做数据同步Replayed 是数据集的标记。如果两个节点的 replayed 一致则说明两个节点是同一个数据集。每一个 master 节点都有唯一的 replayedslave 节点则会继承 master 节点的 replayed。Offset 是偏移量随着记录在内存缓冲区中的数据增多而逐渐增大。slave 完成同步时也会记录当前同步的 offset如果 slave 的 offset master 的 offset说明 slave 数据落后于 master需要更新结论。因此slave 做数据同步必须向 master 声明自己的 replayed 和 offsetmaster 才可以判断到底是全量同步还是增量同步以及增量同步需要同步哪些数据。使⽤ Replication_id 来判断 slave 节点是不是第⼀次来做数据同步如果 replid ⼀样说明不是第⼀次如果 replid 不⼀样说明是第⼀次做数据同步。offset量只能说明 repl_baklog 数据缓冲区中的数据同步的进度。增量同步增量同步在 slave 节点断开又恢复连接主节点时会执行增量同步。流程Slave 请求数据同步Master 判断是否为第一次同步。Master 判断不是第一次同步返回 continue。Master 根据 offset 向 Slave 发送内存缓冲区中的命令Slave 收到命令并执行。