- RDB记录redis快照,一般定期(如60s)执行一次,由于定期间隔长安全性低性能好,压缩二进制文件大小小,恢复快。
- AOF记录操作增量,也可以定期(如1s)执行一次,由于定期间隔短安全性高性能差,每次插入删除修改都要记录文件大,恢复需要逐个执行命令恢复慢
- 由于AOF记录了很多冗余操作(比如插了又删),因此可以用rewrite命令重写AOF文件
- AOF文件不清理一直追加
Redis异步持久化机制:如何利用写时复制保证异步并减少内存占用
- 主进程持久化,调用fork,操作系统将主进程的虚拟内存页表(指向原始物理数据所在页)拷贝给子进程。
- 注意:fork会阻塞主进程因此这里要操作够快
- 将原始物理内存数据加上read-only,并开始持久化
- 期间主进程要修改内存数据(如插入修改删除),OS在对应页表新建一个拷贝副本,并改变主进程虚拟内存页表指向,主进程在新的拷贝副本修改
- 子进程持久化结束,期间被拷贝的原始页(主进程要修改)被回收
这样只将持久化期间需要修改的页进行额外拷贝,其它地方没有多余内存占用。此外fork时只拷贝了虚拟内存页表因此很快,基本不阻塞主进程
- RDB的save 3600 1命令,代表如果 111 小时内有 111 次修改,就触发快照。这个意思是隔一个小时检查一次,如果过去一小时又修改则出发快照,而不是只要有修改就立马触发快照
Redis主从模式下的同步
- Redis主从模式,最初主从节点建立连接,使用全量同步RDB。后续看情况,如果从节点的offset落后主节点太多则继续全量同步RDB,如果从节点offset没落后太多则使用复制积压缓冲区repl_baklog执行增量同步
- 正常状态下,主库每执行一个写命令,就要将该命令发给从库的输出缓冲区,并往repl_backlog中存一份,从库来执行同步
- 异常状态(如从库断开连接),从库恢复后,如果主从offset差距小,主库根据offset与主库的差异,将repl_backlog的对应写命令发给从库,执行增量同步
- 异常状态(如从库断开连接),从库恢复后,如果主从offset差距大,主库直接将RDB发给从库来执行全量同步
复制积压缓冲区repl_baklog与AOF机制很类似,两者什么区别
AOF没有内容量限制,无限写。而repl_backlog是环形缓冲区(存的是主从库差异的写命令),有内容限制,且如果offset差距过大,超过了缓冲区限制则直接使用RDB全量同步
从库输出缓冲区和复制积压缓冲区区别
从库输出缓冲区用于正常状态下的同步,每个从库都有一个;复制积压缓冲区所有从库共享,环形结构
Redis哨兵模式下的消息通知
哨兵与哨兵之间
- 只建立命令连接。在节点频道上打过招呼后可以建立命令连接,通过这个连接执行Gossip协议,判断对方是否还活着PING,用Raft选举领头哨兵等
哨兵与节点之间
- 每个哨兵与所有主从节点之间都建立两个连接:命令连接和订阅连接
- 命令连接用来发送命令,如确认节点是否在线PING,获取从节点信息INFO,故障恢复时领头哨兵发送的SALVEOF NO ONE以及SLAVEOF [IP] [PORT]等
- 订阅连接用来发送频道消息,各个哨兵都向其中发送消息并监听主节点的频道消息,以此来确认其它哨兵信息。节点只作为订阅连接的跳板,不对订阅连接的信息进行转发外的任何操作(甚至都不存储)。
- 之所以每个主从节点都有订阅连接,是防止主节点挂掉导致频道消失
哨兵与客户端之间
-
初始化发现 :客户端(如 Jedis, Lettuce)启动时不再直接配置 Redis 的 IP ,而是配置哨兵集群的 IP 列表 。客户端先连接哨兵,通过发送
SENTINEL get-master-addr-by-name [MasterName]命令,从哨兵手中获取当前主节点的真实 IP 和端口。 -
订阅通知机制 :客户端会与哨兵建立 订阅连接 ,监听特定的频道(最核心的是
+switch-master)。- 当哨兵集群完成故障转移、选出新主节点后,领头哨兵会向该频道发布消息。
- 消息包含:
旧主名称、旧主IP、旧主端口、新主IP、新主端口。
-
自动重连与切换 :客户端捕获到
+switch-master事件后,会立即执行以下动作:- 销毁旧连接:主动断开与原主节点(已挂掉或已降级)的所有连接池。
- 刷新配置:在本地更新主节点的内存地址。
- 重建连接:根据新地址重新建立连接池,确保后续业务请求发送到新主节点。
Redis哨兵模式下的故障恢复
- 主节点挂掉
- 首先哨兵之间存在命令连接,在该连接上首先使用Gossip协议判断主节点是不是真的挂了,如果挂了利用Raft选举算法选取领头哨兵
- 领头哨兵判断哪个从节点为新的主节点,选取规则是:1.长时间PING不同的不选;2.配置中priority最高的从节点;3.选offset最新的从节点;4.选ID最小的从节点
- 领头哨兵选举该从节点为新的主节点:利用命令连接让该从节点执行SLAVEOF NO ONE
- 领头哨兵将其他节点变为该节点的从节点:利用命令连接让这些节点执行SLAVEOF [主节点IP] [主节点PORT]
Redis哨兵模式下的两套频道系统
1.集群元数据频道(sentinel:hello)
- 存在于每个Redis数据节点
- 由所有哨兵进程订阅
- 用于哨兵之间自动发现,同步等
2.事件通知频道
- 存在于每个哨兵进程中
- 由外部客户端(如Jedis)订阅
- 当故障转移完成后,推送消息给每一个订阅的客户端(如主节点变动)
Redis集群模式下的槽
- 槽不对应任何的实际物理内存空间,它的作用其实就是:在插入或读取数据时判断数据位于哪个redis节点中,之后就正常将redis数据存入或读取即可;以及解决数据迁移时的粒度问题(以一个槽为粒度将其对应的所有key和value迁移)
- 为了迁移时对槽整体迁移,需要知道某个槽对应的所有key的位置,这个对应的数据结构就是Slot-to-Dict 数组
- 在Redis7之前Slot-to-Dict 数组采用跳表实现,查找初始槽是logN的复杂度,顺序遍历出后续的数据位置
- Redis7之后的Slot-to-Dict 数组采用顺序表+哈希的结构,比如节点A对应槽0-15,那么A有全量16384行记录的顺序表,前16行为有效数据,每行第一个元素为slotid,第二个元素为对应所有数据所在哈希表的地址
- 在 Redis 7 之后,写入流程:计算槽位 Slot=10Slot=10Slot=10 →\rightarrow→ 访问 Array[10] →\rightarrow→ 发现指针指向一个字典 →\rightarrow→ 将 Key-Value 直接存入该字典。
- 为什么用全量数组,明明大部分位置为Null?因为全量数组也不占用多少空间,且重新分片易于实现,接管一个槽位只需要执行 slot_to_dict[i] = 新字典地址。