持久化
- AOF
- 每次执行更新操作,会将命令追加到 AOF 缓冲区中,通过
write()(每次操作都会执行) 和fsync()(通过写回策略控制执行时机) 刷入磁盘,有不同的写回策略:AlwaysEverySecNo - 当AOF 文件过大时,会触发 AOF 重写 来压缩文件大小,将当前数据库中所有的键值对用命令记录到新的 AOF 文件中,完成后替换旧的 AOF 文件,通过后台子进程
bgrewriteaof完成,利用了子进程的 写时复制 机制,重写过程中的新命令会记录到 AOF 重写缓冲区中,重写完成后将新内容追加到新 AOF 文件末尾
- 每次执行更新操作,会将命令追加到 AOF 缓冲区中,通过
- RDB
- 可以手动执行
save和bgsave,也可以通过配置文件配置bgsave触发时机,触发后会保存数据库二进制快照,bgsave同样 fork 出子进程并利用 写时复制 机制
- 可以手动执行
- AOF + RDB 混合持久化
- AOF 重写期间,不采用命令的方式记录全量数据库,而是采用 RDB 序列化的方式保存二进制快照,重写期间新的命令会记录到 AOF 重写缓冲区,重写完成后将缓冲区中新的命令追加到新 AOF 文件中,最终的 AOF 文件前半部分是 RDB 格式的二进制数据,后半部分的 AOF 格式的命令
- 大 key 对持久化的影响
从以下几个方面考虑:
fsync()写入耗时fork()时复制页表耗时del删除 key 的耗时(应该改用unlink)- 网路传输的带宽限制
- 集群模式中数据分布不均问题
AOF

AOF (Append Only File)
Redis 默认不开启 AOF,通过修改 redis.conf 启用:
// redis.conf
appendonly yes // 是否开启AOF
appendfilename "appendonly.aof" // 持久化文件的名称
以 set name v1hz 为例,AOF 日志内容类似于下面:
*3
$3
set
$4
name
$4
v1hz
*3 表示有三个部分,$3 表示该部分有 3 个字节
Redis 会先执行命令再记录日志,因为
- 避免额外的检查开销
- 不阻塞当前写操作的执行
AOF 的写回策略
redis.conf 中提供了 appendfsync 来配置写入磁盘的时机:
Always:每次操作后都进行write和fsync,最安全EverySec:每次操作后只执行write,每秒执行一次fsync,折中方案,服务器宕机会损失一秒数据No:Redis 不执行fsync,写入磁盘的时机由操作系统决定,服务器宕机会损失所有未持久化的数据
AOF 重写
如果 AOF 文件过大会导致恢复过程很慢,redis 采用 AOF 重写机制来压缩 AOF 文件
- 每当 AOF 大小超过设定的阈值时,就会触发 AOF 重写
- 将当前数据库中的所有键用命令记录到新的 AOF 中,然后替换旧的 AOF 文件
- 为了避免重写失败导致 AOF 文件被污染,所以要新建一个,重写成功再替换
如果执行了 set a 1 和 set a 2,重写后日志中只会记录 set a 2,只记录最终形态,大大压缩了 AOF 的大小
AOF 后台重写
- AOF 重写是由后台子进程
bgrewriteaof完成的,子进程带有主进程的数据副本 - 主进程在重写期间会继续处理写命令,新命令会被同时写入 AOF 缓冲区和 AOF 重写缓冲区
- 子进程完成重写后,向主进程发出信号,主进程接收到信号后会把 AOF 重写缓冲区的新内容追加到新 AOF 文件末尾,然后用新 AOF 文件替换旧的
在一个进程内调用 fork 会创建一个子进程,和主进程共享内存(通过只复制目标内存的页表实现),这块内存会被标记为只读,当其中一个进程要修改数据时,会触发「写时复制」,将需要修改的目标内存页复制一份(只会复制要修改的内存页,其他内存依旧可以共享)
如果用线程而不是子进程,为了避免数据同时被多方修改,就要加锁,性能会严重下降,实现复杂度也会上升
RDB
RDB 的使用
Redis 提供了两个命令来生成 RDB 文件:
save:在主进程中生成 RDB 文件,会阻塞主进程bgsave:在子进程中生成 RDB 文件,不会阻塞主进程 可以在 Redis 配置文件中配置生成 RDB 文件的条件
save 900 1
save 300 10
save 60 10000
save m n 表示在 m 秒内对数据库修改了至少 n 次就会进行一次 bgsave,满足其中一个就会执行
- 和 AOF 后台重写一样,RDB 也是
fork出子进程并利用写时复制,系统恰好在 RDB 文件生成后崩溃,则会丢失主进程在快照期间修改的数据 - 极端情况下,如果快照期间,所有数据都被修改,内存占用会使原先的两倍(所有共享内存都被复制)
AOF+RDB:混合持久化
Redis 4.0 提出混合使用 AOF 和 RDB,也叫混合持久化 Redis 配置文件中开启混合持久化:
aof-use-rdb-preamble yes
- 开启混合持久化后,在 AOF 重写时,
fork出来的子进程会将与主线程共享的内存数据以 RDB 的格式写入文件 - 重写期间主进程进行的操作会记录在 AOF 重写缓冲区,然后将新增的命令以 AOF 格式写入文件
- 这样文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的新增命令,完成后用这个文件替换旧的 AOF 文件
RDB 是紧凑的二进制数据,AOF 是命令日志, 所以本质上使用 RDB 优化了 AOF 的「全量数据表示」部分
大 key 对持久化的影响
- AOF 的
Always策略,每次操作都会调用fsync,写入大 key 耗时变长,阻塞主线程,Redis 响应变慢 - AOF 后台重写和 RDB 后台快照都要
fork进程,复制页表会更慢,同时如果主线程修改大 key 会触发写时复制,复制耗时更长,且内存占用更高(如果开启内存大页会影响 Redis 性能,写时复制会以 2MB 为单位而不是 4KB) - 使用
del删除大 key 耗时长,因为会同步释放内存,应改用unlink异步删除 - 网络压力大,如果一个 key 大小为 1MB,每秒 1000 次请求就是 1000MB/s,千兆网卡无法承载
- 集群模式下,大 key 会导致某些节点内存远高于其他节点,数据分布不均,查询集中在大 key 节点会导致负载倾斜,影响整体的吞吐和稳定性
过期机制
过期删除策略
Redis 内部存储着一个过期字典,包含一个指向键的指针和一个过期时间戳,查询一个 key 时,Redis 会先判断这个 key 是否在过期字典中:
- 如果不存在,则正常返回值
- 如果存在,则获取该 key 的过期时间,与系统时间比对来判断过期
- 定时删除:内存会尽快被释放,内存友好,但 CPU 不友好 在设置 key 的过期时间时,创建一个定时事件,时间到达时,由事件处理器自动执行 key 的删除操作
- 惰性删除:CPU 友好,但内存不友好
不主动删除过期 key,每次从数据库访问 key 时,检测 key 是否过期,如果已过期则删除(通过
lazyfree_lazy_expire参数配置是异步删除还是同步删除) - 定期删除:折中方案 每隔一段时间随机取出一定数量的 key 进行检查,如果过期则删除 数据库每轮随机取 20 个 key,如果已过期 key 超过五个就继续选取 20 个,没超过则停止,为了防止不停循环,定期删除流程耗时设定为不超过 25ms
Redis 实际选择「惰性删除+定期删除」配合使用:
- 一方面会在取值时判断是否过期,如果过期就删除;
- 另一方面会定期采样,将过期数据删除;
内存淘汰策略
如果 Redis 的运行内存超过设定的最大内存,会使用内存淘汰策略删除符合条件的 key
最大内存通过 maxmemory <bytes> 配置,64 位系统中默认为 0,表示没有限制,32 位系统中默认为 3GB
可以通过 config set/get maxmemory-policy 配置和查看内存淘汰策略
noeviction:不进行内存淘汰,如果运行内存达到设置的最大内存时,如果有新的数据写入会报错通知禁止写入
对设置了过期时间的数据淘汰
volatile-random:随机淘汰设置了过期时间的数据
volatile-ttl:淘汰采样中更早过期的数据
volatile-lru:设置了过期时间的数据中,淘汰采样中最久未被使用的数据
volatile-lfu:设置了过期时间的数据中,淘汰采样中最少被使用的数据
对所有范围的数据淘汰
allkeys-random:随机淘汰
allkeys-lru:淘汰采样中最久未被使用的数据
allkeys-lfu:淘汰采样中最少被使用的数据
Redis 不全局扫描所有的 key,而是随机采样多个 key(默认为 5 个,通过配置调整),从中找出需要淘汰的目标数据
LRU 和 LFU
LRU 和 LFU 的实现方式主要依赖于 Redis 对象头中的一个 24bit 结构,如下:

LRU
正常的 LRU 是通过链表实现的,但是 Redis 为了避免引入额外的内存开销,没有选择使用链表,而是利用对象头中的 24bit,存储该数据的最后一次访问的时间戳
这样的实现有以下优点:
- 不用维护链表,节省了内存占用
- 不用每次数据访问时移动表项,提升了缓存性能
LRU 无法解决缓存污染问题,如果一次读取了大量的无用数据,这些数据只会读取一次却会在 Redis 中留存很长时间,挤占真正的热点数据的空间,LFU 则能提高热数据的缓存命中率
LFU
对象头中的 24bit 分两段来存存储,包括:
ldt用 16bit 记录上一次被访问的时间戳(单位为分钟)logc用 8bit 记录该数据被访问的频次 每次数据被访问,logc会变化:
- 按照上一次访问距离当前的时长,对
logc进行衰减 - 按照一定的概率增加
logc,logc越大增加的概率越低 可以在配置文件通过lfu-decay-time和lfu-log-factor配置logc的衰减和增长速度
lfu-decay-time 和 lfu-log-factorlfu-decay-time = 10 表示 logc 每 10 分钟减 1
logc 增加的概率 的计算方式为 p = 1 / (logc * lfu_log_factor + 1)
这样的设计有以下几点好处:
logc是缓慢增加的,8bit (最大 255)也能表示被访问上千次的数据的频次- 根据上一次访问的时间进行衰减,自动淘汰「历史热数据」
分布式锁
分布式锁的多种问题
- 锁争抢:「先查再锁」不是原子操作,通过
SET value NX EX解决 - 僵尸锁:节点崩溃后无法释放锁,通过设置过期时间解决
- 早过期:业务还未完成锁就过期了,通过锁续期解决
- 单点故障:节点崩溃后所有锁操作失效,通过 RedLock 算法解决
原子加锁 —— NX/EX
SET lock_key unique_value NX EX 30
NX:表示只当锁不存在时才加锁
EX:表示 30s 后自动过期
unique_value:识别业务方的唯一 id
锁续期 —— 看门狗机制
- 当业务节点抢到锁后,系统会在这个节点上启动一个后台线程(看门狗),看门狗会按固定时间主动检查,如果发现业务线程还在正常运行,就会把锁的时间延长
- Redisson 框架内置了看门狗机制,当直接
lock()时才会启用看门狗机制,默认的 TTL 为 30 秒,延长时间为原始 TTL 的三分之一(10 秒),这样如果第一次续期失败,还有三分之二的时间供第二次续期;如果lock()指定了 TTL 则不会启用看门狗机制
高可用锁 —— RedLock 算法
- 加锁 部署至少三个独立节点,只有超过半数节点都加锁成功才算拿到有效锁
- 释放锁 先通过锁的 id 值判断是否是自己的锁,然后再释放锁,通过 lua 脚本保证验证和释放是一个原子操作
普通任务 => 单节点锁 + 合理的过期时间 长任务 => 加看门狗实现锁续期 核心业务 => RedLock 实现高可用
高可用
主从复制
Redis6.0+ 将 SLAVEOF 命名为 REPLICAOF,二者等效
Redisd 主从复制和 MySQL 主从复制一样,添加多个服务器作为主服务器的副本,所有写操作都在主服务器上进行,将最新的数据同步给从服务器,做到主从数据一致,主、从服务器都能处理读操作,做到读写分离
第一次同步 —— 全量复制

第一阶段 —— 准备全量复制
每个 Redis 服务器启动后会生成一个随机的
runID来唯一标识自己
- 从服务器执行
replicaof <ip> <port>后,主从服务器建立起 TCP 连接, 然后 Redis 会向主服务器发送psync <runID> <offset>, 由于是第一次同步不知道主服务器的runID所以为?, 同步还未开始所以offset是 -1 - 主服务器接受到从服务器的请求后,返回
FULLRESYNC <runID> <offset>,FULLRESYNC表示主服务器要进行一次全量复制,将所有数据同步给从服务器 - 从服务器接收到响应后,保存主服务器的
runID和同步进度offset
第二阶段 —— 同步快照数据
- 主服务器执行
bgsave生成 RDB 快照文件,并将文件发送给从服务器 - 从服务器接收到文件后,会清空当前数据,然后载入 RDB 文件
- 从服务器载入完成后,会返回一个确认响应给主服务器
第三阶段 —— 同步最新数据
主服务器会在以下时间间隙将新的写命令写入 replication buffer 缓冲区中:
- 主服务器生成 RDB 文件期间
- 主服务器发送 RDB 文件期间
- 从服务器载入 RDB 文件期间 主服务器接收到从服务器载入完成的确认消息后,就会将缓冲区中的新命令发送给从服务器,从服务器执行所有的新命令后,主从服务器数据就一致了,第一次同步完成
后续的同步 —— 命令传播
第一次同步完成后,TCP 连接会一直维护着,后续主服务器会通过这个连接继续将新的写命令同步给从服务器,这是 基于长连接的命令传播,避免频繁的 TCP 连接断开和建立带来的性能损耗
网络断开重连后的同步 —— 增量复制
如果网络连接断开,命令传播就无法继续,网络连接恢复后,主从服务器数据不一致
Redis2.8 之前,网络连接恢复后,主从服务器会重写进行一次全量复制
Redis2.8 之后,采用 增量复制 的方式继续同步
增量复制的实现主要依赖以下几个数据:
repl_backlog_buffer:一个环形的缓冲区,主服务器在命令传播时,不仅将命令发送到从服务器,还会写入这个缓冲区replication_offset:master_repl_offset用于记录主服务器写到哪里,slave_repl_offset用于记录从服务器读到哪里repl_backlog_start_offset:用于记录主服务器中环形缓冲区的起始命令的偏移量
当网络恢复后,从服务器发来 psync <runID> <slave_repl_offset>
主服务器计算 slave_repl_offset >= repl_backlog_start_offset:
- 如果为
true说明从服务器要读取的数据在缓冲区中,用CONTINUE命令告诉从服务器接下来使用增量复制的方式同步数据,增量数据会写入replication buffer通过命令传播发送给从服务器 - 如果为
false则说明不在缓冲区中,接下来会进行一次全量复制
repl_backlog_buffer 缓冲区大小的设置缓冲区大小可以根据公式
repl_backlog_buffer_size = seconds * write_size_per_second 计算
seconds 表示从服务器断线后重连上主服务器的预估秒数
write_size_per_second 表示主服务器每秒产生的写命令数据大小
具体的大小可以设置为以此为基础的两倍,用于预防特殊情况
分摊主服务器压力
一个服务器执行 replicaof <ip> <port>,如果目标 IP 的服务器是从服务器,目标服务器会成为「中间主服务器」,当前服务器会成为「下一级从服务器」
这样做有以下几个好处:
- 减少主服务器
bgsave的次数(Redis 主从复制不会复用同一份 RDB 文件) - 节省主服务器的网路带宽
面试题
如何判断 Redis 某个节点是否正常工作?
Redis 通过互相的 ping-pong 心态检测机制来检查节点是否正常工作,如果一半以上的节点 ping 一个节点没有 pong 响应,集群就会认为这个节点挂了,断开和这个节点的连接
- 主节点每 10 秒向所有从节点发出 ping 命令,判断从节点的连接状态,可以通过
repl_ping_slave_period控制发送间隔 - 从节点每 1 秒向主节点发送
replconf ack <offset>命令,上报自身当前的复制偏移量,以此实现:- 检查主从节点之间的连接状态
- 检查复制数据是否丢失,如果丢失则重新从主节点拉取丢失数据
主从复制架构中,过期 key 如何处理?
某个 key 被删除有多个时机:
- 显示执行
del命令 - 到期被定期/惰性删除
- 内存满了,淘汰算法淘汰
主节点会模拟一个
del命令发送到从节点
Redis 是同步复制还是异步复制?
Redis 接收到写命令后,同步写入内部缓冲区,然后异步发送给从节点
主从复制中的 replication_buffer 和 repl_backlog_buffer 的区别?
- 一个主节点只分配一个 replication backlog buffer;主节点会给每个从节点分配一个 replication buffer
- replication backlog buffer 是一个环形缓冲区,满了后会覆盖旧数据;replication buffer 满了会断开连接,删除缓存,从节点重新连接并重新进行全量复制
如何应对主从数据不一致?
主从数据不一致主要是因为,主节点的命令传播是异步进行的,主节点不会等待从节点执行完命令再返回给客户端,所以可能主节点已经返回,而从节点还未执行完成命令。应对方法如下:
- 尽量保证主从节点之间的网络连接良好,避免主从节点在不同机房
- 开发一个监控程序,当主从节点之间的同步进度差值大于预设阈值,就阻止客户端和读取目标从节点的数据。为了避免客户端和所有从节点都不能连接,要把阈值设置的大一些
主从切换如何减少数据丢失?
主从切换时有两种原因导致数据丢失:
- 异步复制断开:主节点的数据还未同步给从节点就宕机,未同步的数据丢失
- 集群脑裂:主节点网络出错,和从节点断联,但依旧能和客户端连接,哨兵选出一个新的主节点,网络恢复后出现旧的主节点被降级为从节点,全量复制前清除数据,导致数据丢失
解决方案:
- 通过
min-slaves-to-write n保证主节点至少要有 n 个从节点连接,才允许写操作 - 通过
min-slaves-max-lag m保证主从复制的延迟至少要低于 m 秒,才允许写操作
当客户端发现主节点不可写后,可以先将数据写入本地缓存,或者将数据写入 kafka 消息队列,恢复正常后再重新写入主节点
哨兵(高可用)
哨兵在监控主节点并触发故障转移,先后会有两次投票:
- 判断主节点的投票
- 选举 leader 的投票
「主观下线」和「客观下线」
- 哨兵会每 1 秒向所有主从节点发送 ping 命令,如果一个节点没有在规定时间(通过
down-after-milliseconds配置,单位毫秒)内响应,该哨兵就会将这个节点标记为「主观下线」 - 当一个哨兵将主节点标记为「主观下线」后,会向其他哨兵发起命令(第一次投票),其他哨兵收到后,会做出赞成/拒绝投票的响应,当赞成票数超过配置文件中的
quorum值后,就会将主节点标记为「客观下线」
主节点下线会触发故障转移,为避免误判必须让多个哨兵共同确认 从节点故障影响小,无需多哨兵确认
哨兵中的 leader 竞选
主节点故障后,只能由一个哨兵来进行故障转移,所以需要选举出一个哨兵作为 leader
- 率先将主节点标记为「客观下线」的哨兵,成为「候选者」
- 「候选者」会向其他哨兵发起命令(第二次投票),要求进行投票选取 leader 执行故障转移
- 赞成票 超过半数哨兵,并且超过
quorum,「候选者」就能成为 leader
quorum 的设置、哨兵的数量- 如果两个哨兵同时将主节点标记为「客观下线」就会出现多个候选者,这时谁的命令先到达其他哨兵,就会先获得赞成票,后到的命令会被拒绝投票
quorum应该设置为sentinel_nums / 2 + 1- 哨兵个数应该大于等于 3 且为奇数,既能容忍部分哨兵故障,还能避免平票
故障转移的过程

一、选出一个新主节点
- 过滤掉已下线或网络不稳定的节点
- 依次判断:
- 优先级:
slave-priority值越小越优先(根据服务器性能手动设置的优先级) - 复制进度:
slave_repl_offset越接近master_repl_offset越好 - 前两个都一样则选取 id 更小的
- 优先级:
- 向目标从节点发送
SLAVEOF no one,将其升为新主节点
发送 SLAVEOF no one 命令后,leader 哨兵会每秒向新主节点发送 INFO 命令查看其节点信息(故障转移之前是 10 秒一次)
当节点的角色信息从 slave 变为 master 后,leader 哨兵就知道目标节点已经升级为了新主节点
二、让从节点复制新主节点
leader 哨兵会向其他所有从节点发送 SLAVEOF <new_master_ip> <new_master_port> 命令,使他们全部转为复制新主节点
三、客户端更新连接地址
客户端和哨兵建立连接后,会订阅哨兵提供的频道,从指定频道中获取新的 IP 地址和端口号,自动更新连接地址
哨兵提供的消息订阅频道,常见的如下:

四、将旧主节点切换为从节点
当旧主节点重新上线后,哨兵集群会向其发送 SLAVEOF 命令,使其变为从节点
哨兵集群的组成方式
哨兵节点通过订阅主节点上的频道交换彼此的信息,从而实现互相发现
当添加哨兵时,只需要配置主节点名、主节点 IP 地址和端口、quorum 值:
sentinel monitor <master_name> <master_ip> <master_port> <quorum>
- 主节点有一个名为
__sentinel__:hello的频道,哨兵连接到主节点后,每个哨兵会每秒向这个频道发送一条消息,包含自己的 IP、端口、runID 等,其他哨兵会订阅这个频道并从中获取新哨兵的 IP 和端口,跟新哨兵建立 TCP 连接 - 每个哨兵和主节点建立连接后,会每 10 秒向主节点发送
INFO命令,从而获取从节点信息列表,和每个从节点建立连接并持续监控
Cluster 集群(高可用 + 高扩展)
Cluster 集群模式中内置高可用,没有哨兵这种角色
下文的「节点」一般指「由多个节点组成的主从结构的分片」
Cluster 集群主要解决以下几个问题:
- 高扩展
- 如何将数据映射到分片
- 查询数据时如何访问正确的分片
- 高可用
- 节点之间如何通讯
- 节点出现故障怎么处理
数据到分片的映射方案 —— 哈希槽
Redis Cluster 采用哈希槽来将数据映射到目标分片
- 一个集群的拥有 16384 个槽位
- 当插入数据时,使用 CRC16 对 key 进行计算,然后对 16384 取模得到目标槽位
- 每个分片负责一部分槽位,例如 A 负责 0
5460 号,B 负责 546110922 号,C 负责 10923~16383 号,取模结果根据槽位信息存储到对应分片中
- Cluster 集群的目标是高可扩展,后续必然会增加/减少分片
- 模数会变化会导致几乎所有分片中的数据都需要频繁迁移,性能损耗大
- 哈希槽将模数固定为 16384,增减分片时只需更新分片的槽位信息并迁移部分数据即可
读取时,客户端通过本地槽位信息,或者通过分片返回的重定向信息确认目标数据所在槽位
读取数据的智能寻址 —— MOVED/ASK 重定向
一个节点对请求的处理流程如下:
- 计算哈希槽映射,如果目标 key 应在的哈希槽不是自己负责的,则返回
MOVED重定向;如果自己负责目标 key 应在的哈希槽,则判断该哈希槽是否正在迁出或导入 - 如果正在迁出,若 key 存在则返回;不存在则返回
ASK重定向 —— 客户端接收到ASK重定向后,会对重定向的目标节点发送ASKING命令,为其添加标记 - 如果正在导入,检查是否带有
ASKING标记,如果有则查找 key 并返回结果;否则返回MOVED重定向 —— 客户端收到MOVED重定向后,会更新本地槽位信息 - 如果哈希槽无迁出/导入状态,则直接查找 key
各节点之间的通信机制 —— Gossip
每个节点周期性地从节点列表中选择 k 个节点,将本节点存储的信息传播出去,直到集群的信息一致 Gossip 通过 集群总线 进行通信,即每个节点都会多开一个端口,端口号为 对外服务端口号 + 10000,通信采用特殊的二进制协议 传播的消息有多种:
meet:通知目标节点加入集群,目标节点接收到meet消息后会加入集群并开始周期性的传播信息ping:每秒会向其他节点发送ping消息,消息内包含自己已知的两个节点地址、槽位、状态信息、最后通信时间等pong:用于响应meetping消息,消息中同样包含自己已知的两个节点的信息fail:当某个节点判定集群内另一个节点下线后,会向集群广播一个fail消息,其他节点收到后,把对应的节点状态更新为下线状态
集群内的故障发现和转移
故障发现
- 当一个节点
ping另一个节点后发现响应超时,则会将该节点标记为「主观下线」 - 在后续的
ping消息中会将这个信息传播给其他节点,其他节点接收到后也会检查异常节点的状态 - 如果超过半数的节点将异常节点标记为「主观下线」,就会将目标节点标记为
FAIL「客观下线」 故障转移 当从节点检测到主节点被标记为FAIL后,会自动开始故障转移
- 资格检查:筛选出正常状态的、和主节点保持较新复制的从节点
- 发起选举:符合资格的从节点在一段延迟后(基于复制偏移量,数据越新延迟时间越短),向其他所有主节点请求投票
- 晋升:获取超过半数投票的节点胜出,执行
SLAVEOF no one,升为主节点 - 集群同步:新主节点通过 Gossip 广播自己的新角色信息
数据越新延迟时间越短,这奖励了数据更新的节点,同时间接奖励了网络更好的节点,使得更优秀的节点更容易获得投票
缓存
缓存雪崩
主要是由于大量数据同时过期或者 Redis 故障宕机,导致大量请求直接访问数据库,如果数据库撑不住就会导致连锁反应,出现「雪崩」
- 大量数据同时过期
- 均匀设置过期时间:为过期时间加上一个随机数
- 互斥锁:如果发现访问的数据不在 Redis 中,就加一个互斥锁,保证同一时间只能有一个请求构建缓存,构建完成后再释放锁,未能获得锁的请求,要么等锁释放后读取缓存,要么直接返回空值
- 后台更新缓存:缓存永不过期,业务只查缓存,但是当内存占用过高时部分缓存会被淘汰。
- 第一种解决方案:定时检测是否有缓存失效,或者等业务线程发现有缓存失效,如果失效就从数据库中读取并更新到缓存,但是定时检测中间会有窗口期;
- 第二种解决方案:在缓存过期前,通知后台线程更新缓存并重新设置过期时间
- Redis 故障宕机
- 服务熔断或请求限流:当检测到 Redis 宕机时,一方面可以启用服务熔断,暂停业务对缓存服务的访问,另一方面可以允许少量请求发送到数据库,让业务在没有缓存的状态下低速运行
- 构建 Redis 高可用集群:当主节点宕机后,从节点可以切换为主节点继续提供缓存服务
缓存击穿
某个热点数据过期,也会导致大量请求达到数据库,可以采用与缓存雪崩类似的解决方案
- 互斥锁:同一时间只允许一个业务线程构建缓存
- 热点数据不设置过期时间:由后台异步更新缓存,或者在热点数据要过期前,通知后台线程更新缓存并重新设置过期时间
缓存穿透
如果要访问的数据不存在,这个数据即不在缓存也不在数据库,请求会一直访问数据库,导致数据库压力骤增,发生这种情况一般有两种可能:业务误删除和恶意攻击
- 非法请求限制:检查请求参数是否合理
- 缓存空值或默认值:对于查不到的数据,缓存一个空值或默认值
- 使用「布隆过滤器」快速判断数据是否存在
布隆过滤器
布隆过滤器由「一个位图」和「N 个哈希函数」组成
- 写入
- 用 N 个哈希函数计算需要写入的数据,得到 N 个哈希值
- 对每个哈希值取模,确认在位图中的位置
- 将这些位置设为 1
- 查询
- 用同样的 N 个哈希函数计算得到 N 个哈希值,取模并找到位图的 N 个位置
- N 个位置中只要有一个不是 0,则一定不存在
- 如果全为 1,则可能存在
缓存一致性
旁路缓存策略(Cache-Aside)

- 写数据时,先更新数据库,然后删除缓存
- 读数据时,缓存不存在,读数据库,更新缓存,缓存设置为较短的过期时间,即使出现并发问题导致数据不一致也会快速过期
相较于删除缓存,更新数据库更耗时,更有可能出现并发问题!
先更新数据库后删缓存也会有并发问题,只是概率小很多
确保更新和删除都成功
如果删除缓存失败,也会导致数据不一致,解决的关键是异步操作缓存+失败重试

- 消息队列重试操作
删除操作改为发送消息到消息队列,如果失败则重试,这个方案对业务代码侵入性较大

- 订阅 Bin Log 通过 Canal 中间件,订阅 MySQL 的 BinLog,检测到有数据更新就发送消息到消息队列,删除对应的缓存,删除成功才返回 ACK,失败则重试。不侵入代码,但引入了新组件。
常见面试题
数据结构
常见的数据结构是怎么实现的?
String:通过 SDS 实现,SDS 有以下特征- 不仅可以保存文本数据,还能保存二进制数据,因为 SDS 判断字符串是否结束,使用
len属性而不是空字符;C 语言的字符串使用空字符(\0)来判断,二进制数据中可能会出现多个空字符所以 C 语言的的字符串无法保存二进制数据 - SDS 获取字符串长度的复杂度是 ,因为内部维护
len属性,C 语言通过遍历所以是 - SDS 的 API 是安全的,字符串拼接不会导致缓冲区溢出,SDS 在拼接字符串前会检查空间是否满足要求,如果空间不足就会扩容
- 不仅可以保存文本数据,还能保存二进制数据,因为 SDS 判断字符串是否结束,使用
List:Redis 3.2 之前,列表元素少于 512 个、每个元素小于 64B 时,使用压缩列表,不满足以上条件时使用双向链表;Redis 3.2 之后则只用 QuickList 作为底层数据结构Hash:哈希元素小于 512 个、每个值小于 64B 时,使用压缩列表,不满足以上条件时,使用哈希表Set:元素都是整数、元素个数小于 512 时,使用整数集合,不满足时使用哈希表Zset:元素个数小于 128 个、元素值小于 64B 时,使用压缩列表,不满足时使用跳表
Redis7.0 之后,压缩列表被弃用,全部使用 listpack 代替
Redis 线程模型
Redis 是单线程吗?
Redis 的「接收客户端请求 → 解析请求 → 执行操作 → 返回数据给客户端」是由一个线程(主线程)完成的,但是 Redis 程序不是单线程的,Redis 在启动时会创建后台程序:
- Redis2.6时,创建两个后台线程,分别处理关闭文件、AOF 刷盘
- Redis4.0后,新增一个线程用于异步释放内存,也就是
lazyfree线程,在执行unlink/flushdb/flushdb命令时由该线程执行