Redis底层实现原理之数据接收同步和高可用持久化机制

1. 单线程模型

单线程模型执行流程:

  1. 阻塞等待连接事件并处理Socket读事件
  2. 解析命令
  3. 执行命令
  4. 处理Socket写事件
  5. 继续循环1步骤

单线程模型指的是2-3步骤只会由单线程同步执行

线程处理情况:

  1. 网络IO
    • 6.0之前:单线程处理Sokcet的读写
    • 6.0+:多线程处理Socket读取和Socket写入响应
  2. IO多路复用:IO多路复用在2.0引入,单一主线程通过多路复用管理所有客户端连接,并把就绪的Socket给后续流程进行读写
  3. lazy free:4.0引入,使用异步惰性删除机制释放内存,避免主线程阻塞
    • 删除大键:unlink key,异步删除大量元素的hash或list
    • 清空数据库:flushdb/flushall async,异步清空整个数据库或所有数据库
    • 内存到达上限淘汰:配置lazyfree-lazy-eviction yes,内存达到maxmemory时淘汰键值对
    • 删除过期键:配置lazyfree-lazy-expire=yes,删除过期key

网络IO多线程实现方式:使用两组生产者+消费者模型实现

  1. 第一组
    • 生产者:主线程,通过IO多路复用获取可读Socket后,把Socket分配给IO线程池
    • 消费者:IO线程池,读取被分配的Socket,解析协议,获取完整命令信息
  2. 第二组
    • 生产者:IO线程池,各个IO线程把完整命令信息放进全局等待队列
    • 消费者:主线程,读取全局等待队列消息,读取后单线程处理命令

读请求和写请求堆积场景:

  • 堆积原因
    • 读请求
      • 写入BigKey:一次性写入大量数据或复杂结构
      • 主线程阻塞:慢查询或LUA脚本执行时间过长
      • 突发请求:突发大量请求
    • 写请求
      • 客户端接收缓慢:响应时客户端处理缓慢
      • 频繁monitor命令:monitor命令会输出大量数据,挤满输出缓冲区
      • 获取大key数据:获取key的数据体积庞大
      • 输出缓冲区过小:未设置合理缓冲区大
  • 处理方式
    • 强制关闭连接:输入或输出区挤满后会关闭对应客户端连接
  • 预防措施
    • 读请求
      • 避免BigKey:设计优化数据结构,避免单键值过大或集合元素过多
      • 启用异步线程:启用相关异步线程,减小主线程性能压力
      • 请求控制:避免死循环或频繁调用
    • 写请求
      • 合理设置输出缓冲区:响应时客户端处理缓慢
      • 避免monitor命令:高流量时期慎用
      • 避免大key数据:避免查询大key或一次性返回过多数

lazy free的核心思想是将耗时且可能阻塞Redis主线程的内存释放操作,交给后台特定的线程异步完成,确保主线程的高性能

lazy free核心实现

  • 数据结构:等待任务队列
  • 处理流程
    1. 投递任务:主线程需要释放大量内存时,创建任务并投递到对应的任务队列
    2. 监听消费:专门的bio_lazy_free线程持续监听队列进行消费,根据任务类型进行响应操
  • 缺陷
    • 延时性:内存不会立即释放,若内存紧张时异步释放可能跟不上新增数据速度
    • 额外消耗:另开线程会额外占用CPU和IO资源

Redis的所有操作都在内存中,一般性能瓶颈在于网络IO和内存带宽,单线程执行命令可以带来以下好处:

  1. 避免锁竞争:简化了数据结构的实现,彻底消除了线程间竞争锁带来的性能损耗
  2. 降低上下文切换:减少了多线程环境下CPU间切换带来的开销,提供CPU缓存命中率
  3. 原子性保证:所有命令执行都是顺序且原子性的,无需考虑并发问题
  4. 代码简洁和可维护性:避免了多线程编程带来的复杂性

2. IO多路复用

Redis通常部署在Linux上,使用的IO多路复用器是epoll,相对select/poll性能更出众

核心数据结构:

  • 红黑树(RB-Tree)
    • 数据结构:平衡二叉树
    • 节点信息:包含要监控的文件描述符(FD)和事件类型
    • 优势:红黑树的插入、删除、查找时间复杂度都是O(logn)
  • 就绪链表
    • 数据结构:双向链表
    • 节点信息:存放已就绪的FD
    • 优势:查询就绪Socket时,只需要检查该链表,无需遍历所有FD,时间复杂度接近O(1)

高效的核心机制

  1. 网卡收到数据后,数据被拷贝到接收缓冲区
  2. 处理完数据包后,触发对应注册FD的回调函数
  3. 回调函数在红黑树中查找对应节点,并将节点添加到就绪链表
  4. 主线程阻塞读取就绪链表的Socket

FD信息生命周期

  1. accept建立新连接后把FD插入红黑树中
  2. FD注册回调函数
  3. 当触发下列操作时,删除红黑树中的FD:
    • 客户端主动断开:当客户端调用close()或终止进程时,Redis清理连接资源
    • 服务器主动关闭:空闲超时、释放资源或强制关闭连接时触发
    • 解析读取Socket失败:协议解析错误、认证失败时

3. 指令处理

3.1 指令解析和执行

统一使用RESP(Redis Serialization Protocol)协议解析参数,随后根据首位参数从命令表中获取对应的实现函数

指令执行:

  1. 普通指令
    • 支持指令:非事务指令和LUA脚本指令
    • 执行方式:立即执行client->cmd->proc(client)
    • 错误处理:命令失败立即返回
    • 网络开销:一次网络开销
  2. 事务指令
    • 支持指令:MULTI和EXEC指令
    • 特殊处理:MULTI指令开启事务后,中途指令仅查询校验,通过后入队
    • 执行方式:接收到EXEC指令后顺序执行队列指令
    • 错误处理:事务入队错误时放弃整个事务,运行错误继续执行后续指令
    • 网络开销:和指令数量成正比
  3. LUA脚本
    • 支持指令:EVAL指令
    • 特殊处理:需要额外解析语法和内部基础执行指令
    • 执行方式:额外声明参数存储结果,执行指令同普通指令
    • 错误处理:脚本命令失败后终止,但已执行指令不回滚
    • 网络开销:一次网络开销

总流程示意图:

客户端请求
传输与读取
经TCP传输, RESP协议编码
协议解析
解析参数至 argc/argv
命令查找
根据argv0查找命令表
普通命令
SET/GET/HSET等
事务命令
MULTI/EXEC/WATCH
Lua脚本
EVAL/EVALSHA
立即执行
调用对应的proc函数
入队或执行
队列或原子执行
脚本执行
解析并原子执行所有操作
返回响应
结果经RESP编码返回
输出缓冲与网络回传
存入输出缓冲区, 事件驱动写回

3.2 指令发送和重定向

在Redis中,主节点接收处理读写指令,从节点只接收读指令,接收写指令将会返回对应错误信息

实现方式

  1. 主从模式
    • 客户端连接节点:分别配置主从节点,主节点配置在首位
    • 写指令处理:必须发给主节点;若发送到从节点会返回仅读错误
    • 关键机制与细节:从节点只读,无自动故障转
  2. 哨兵模式
    • 客户端连接节点:最少可配置一个哨兵,从哨兵获取当前主从节点信息
      • 补充:最好配置所有哨兵地址,无论哪个哨兵宕机都可获取主从节点信息
    • 写指令处理:应发给主节点;若发给从节点会触发重定向
    • 关键机制与细节:哨兵监控主从节点状态,重定向后获取新的主从节点信息
    • 重定向信息:-MASTERDOWN,主节点下线但未完成切换时返回,此时没有节点可以处理写指令
  3. 集群模式
    • 客户端连接节点:最少可配置一个任意节点,从节点获取集群信息,客户端需支持集群协议
      • 补充:最好配置所有节点地址,这样无论哪个节点宕机都可获取集群信息
    • 写指令处理:必须发送给key对应的哈希槽,若发送错误会触发重定向
    • 关键机制与细节:数据分片在16384个槽,客户端缓存槽位映射
    • 重定向类型
      • MOVED重定向:写指令发送给错误节点,客户端收到后会更新本地槽位映射
      • ASK重定向:集群正在分片,会和新的目标节点建立临时连接并先后发送ASKING和原写指令,但不会更新本地槽位映射

4. 部署方式

部署方式

  1. 单机模式
    • 机器要求:1台
    • 特点:部署简单
    • 适用场景:开发测试或小型应用
  2. 主从模式
    • 机器要求:2台
    • 特点:读写分离,但需手动故障转移
    • 适用场景:读多写少,对高可用要求不高的业务
  3. 哨兵模式
    • 机器要求:3台,生产最好5台(主从+3台哨兵)
    • 特点:主从模式基础上引入哨兵,支持自动故障转移
    • 适用场景:支持高可用,但数据量没达到要求分片的业务
  4. 集群模式
    • 机器要求:6台(3主3从)
    • 特点:使用哈希槽数据分片存储,自动故障转移
    • 适用场景:大规模数据、高并发和高可用的业务

从性价比角度考虑:

  1. 单机模式:开发测试
  2. 主从模式:满足大部分业务
  3. 哨兵模式:不推荐,机器少了和主从模式拉不开差距,机器多了不如集群模式
  4. 集群模式:性能要求较高业务

5. 数据同步

数据同步是主从节点间的数据一致性保证,同步机制和部署方式无关

核心概念

  1. 复制ID
    • 存储位置:主、从节点
    • 作用:标识当前主从数据版本
    • 特点:主节点重启或晋升后会生成新ID,并保存上一个复制ID
  2. 复制偏移量
    • 存储位置:主、从节点
    • 作用:判断主从数据一致性
    • 特点:主节点偏移量随发送字节增长,从节点偏移量随接收字节增长
  3. 复制积压缓冲区
    • 存储位置:主节点
    • 作用:缓存近期写指令,方便从节点同步近期部分数据
    • 特点:固定大小环形队列(默认1MB),写满后新数据会覆盖旧数据
  4. 复制缓冲区
    • 存储位置:主节点,每个从节点都会维护一个
    • 作用:全量同步期间临时存储新的写指令
    • 特点:仅在全量同步时使用,完成全量同步后将会把积累的指令发送给从节点

数据同步核心机制:数据同步机制由Redis自动管理,非显式选择增量或全量同步

  1. 触发机制:从节点发送PSYNC请求,主节点对比复制ID和复制偏移量
  2. 增量同步
    • 条件:主从节点的复制ID相同,且复制偏移量复制积压缓冲区范围内
    • 同步内容复制积压缓冲区中在复制偏移量之后的所有写指令
  3. 全量同步
    • 条件:满足其一即可触发
      1. 从节点首次连接主节点
      2. 主从节点的复制ID不同
      3. 复制偏移量未在复制积压缓冲区范围内
    • 同步内容
      • RDB快照文件:为数据差异的从节点执行bgsave生成RDB快照并直接发送
      • 增量写指令:生成RDB快照期间,主节点接收写指令后放入从节点对应的复制缓冲区,发送完RDB快照再把复制缓冲区的写指令发送给从节点

6. 持久化

持久化是节点数据安全可靠的保证

持久化和数据同步差异:

  • 持久化机制:专注于单个节点的数据持久化,偏重数据安全可靠
  • 数据同步机制:专注于主从节点间的数据同步,偏重数据一致性

核心概念

  1. Copy-on-Write(COW,写时复制):RDB模式下,生成RDB文件时主进程和子进程会共享内存数据页,主进程接收写指令执行完后的结果子进程可以立即感知
  2. AOF缓冲区:主进程接收到写指令后写入缓冲区,AOF持久化时把缓冲区的指令写入AOF文件
  3. AOF重写缓冲区:在进行AOF重写时,写指令会被AOF重写缓冲区,当完成AOF重写后把AOF重写缓冲区指令追加到新的AOF文件末尾

实现机制

  1. 全量模式(RDB):特定时间点生成整个数据集的二进制快照保存信息
    • 触发机制
      • 自动触发:配置save规则,如save 900 1为900秒内有1个键被更改则触发
      • 手动触发:执行save(阻塞主进程,慎用)或bgsave(异步子进程,推荐)指令
    • 优点
      • 文件体积小:二进制文件紧凑,便于传输和备份
      • 恢复速度快:直接载入文件,是和灾难恢复和快速重启
      • 对主进程影响较小:持久化由子进程完成,fork时可能阻塞
    • 缺点
      • 数据丢失:快照之间相隔较久,会因宕机丢失
      • fork阻塞:数据集很大时,fork子进程耗时较长,导致内存占用上升
  2. 增量模式(AOF):记录每一个写指令到日志文件
    • 工作流程
      1. 写指令追加到AOF缓冲区
      2. 根据配置策略将缓冲区数据同步到磁盘
        • always:每次写命令都同步,数据安全型,性能最低
        • everysec(默认):每秒同步一次,数据安全和性能平衡型
        • no:由操作系统决定同步时机,性能型,但会丢失较多数
    • 优点
      • 数据可靠性高:使用默认策略,通常最多丢失1秒数据
      • 可读性相对高:AOF文件以文本格式记录,便于人工查看
    • 缺点
      • 文件体积较大:记录每条指令,即使重写后仍比RDB文件大
      • 数据恢复速度较慢:需要逐条执行指令,数据集大时恢复耗时
      • 写入性能受策略影响:always策略对性能影响较大
    • AOF重写(rewriting):fork子进程,根据当前数据库状态生成新的最小命令集AOF文件
      • 目的:解决AOF文件膨胀问题
      • 触发
        • 自动触发:根据时间配置auto-aof-rewrite-percentage和根据大小配置auto-aof-rewrite-min-size
        • 手动触发:发送bgrewriteaof指令触发
  3. 混合模式(RDB+AOF):结合RDB和AOF的优点
    • 工作原理
      1. 触发AOF重写时,不再生成纯命令格式的AOF文件
      2. 子进程先以RDB格式将内存中全量数据写入新AOF文件
      3. 重写缓冲区增量写命令(AOF格式)追加到AOF文件末尾
      4. 最终生成的AOF文件以RDB为头,AOF为尾的混合体
    • 优点
      • 快速恢复:恢复时先加载RDB部分快速恢复大部分数据,再执行增量AOF命令
      • 数据丢失少:增量部分保证了两次RDB快照之间的数据安全性,丢失风险极低
    • 特点
      • 混合模式在AOF重写时生效,正常的AOF流程不变
      • 新的AOF文件人工可读性变差
      • 生成RDB快照时依然会有COW机制保证主子进程内存数据一致
    • AOF重写时的数据一致性保证
      • AOF文件情况:AOF重写时会有两个AOF文件,原AOF文件A,新AOF文件B
      • AOF缓冲区:服务于原AOF文件A,并按策略持久化到原AOF文件A,以保证重写时宕机使用原AOF文件A恢复时不会丢失数
      • AOF重写缓冲区:临时记录增量写指令,服务于新AOF文件B,以保证完成重写后使用新AOF文件B替换原AOF文件A的数据完整性

7. 高可用

Redis的哨兵模式和集群模式可支持高可用

7.1 哨兵高可用

哨兵模式高可用理论最佳是5台机器:1主1从3哨兵

推荐哨兵数量:大于等于3且为奇数

核心概念

  • 投票请求SENTINEL is-master-down-by-addr命令
  • 主观下线:哨兵通过向主节点发送PING命令,若在down-after-milliseconds(默认30秒)内未收到回复,该单个哨兵会标记主节点主观下线
  • 客观下线:当某个哨兵认为主节点主观下线后,会向哨兵集群发送投票,若赞成票≥quorum值,主节点标记为客观下线
  • quorum值:判断主节点是否客观下线所需的最小哨兵同一数量,通常为N/2 + 1
  • 任期号(epoch):单调递增的计数器,代表哨兵当前选举轮次
  • 候选者:当哨兵把主节点标注为主观下线后,会向其它哨兵发送投票请求,只要发送了投票请求,即当作获得候选者资格

哨兵leader选举

  • 必要性:主节点被判定为客观下线后,需要有一个leader来负责执行故障转移
  • leader有效性:每次需要故障转移时都会重新选举出新的leader,好处有二:
    1. 负载均衡:每次故障转移都使用不同的哨兵当leader,避免单一哨兵处于高负载状态成为瓶颈
    2. 简化设计:哨兵本就用于监控主从节点,如果又要维护哨兵中leader的身份,得不偿失
  • leader诞生条件:两个条件都须满足
    1. 超过半数:获得赞成票>总哨兵数/2
    2. quorum值:获得票数≥quorum值
  • Raft算法:触发主观下线后,用以从哨兵中选举出leader
    • 投票判定
      • 先到先得,先接收到A哨兵的投票请求,就会把票投给A哨兵
      • 若同时到达,则随机投给其中一个
    • 选举轮次失败:当同一个epoch投票未产生leader,各个哨兵会等待随机时间,随后发起新一轮选举,直到成功选举
    • 算法流程
      1. 哨兵检测到主节点主观下线,向其它哨兵发送投票请求,同时投自己一票,成为候选者
      2. 哨兵接收到其它哨兵的投票请求时,根据投票判定判断谁能当leader
      3. 当满足leader诞生条件时当选新的leader

哨兵变动

  • 启动哨兵:通过读取配置文件获得初始主节点信息,并从主节点获取所有从节点信息,并持久化到本地配置文件
  • 新增哨兵:配置自动发现并监控指定主节点,其它哨兵通过主节点得知新哨兵节点
  • 删除哨兵:哨兵通过PING命令探测其它哨兵是否下线,若下线总有效节点减少

哨兵leader推举主节点条件:

  1. 排除不健康的节点:过滤掉本身已经下线或经常断线的从节点
  2. 优先级:检查从节点的replica-priority配置值,数值越小,优先级越高
  3. 复制偏移量:若优先级相同,选择复制偏移量最大的从节点(偏移量越说明数据越新,和原主节点差异最小)
  4. 运行ID(Run ID):以上条件都相同,选择运行ID最小的,Run ID是启动时自动生成的唯一标识

故障转移流程

  1. 筛选主节点:根据推举主节点条件筛选出最适合当选新主节点的从节点
  2. 推举主节点
    1. 哨兵leader向从节点发送SLAVEOF NO ONE命令
    2. 从节点停止从旧主节点同步数据,将自己转变为主节点
    3. 哨兵leader以每秒1次的频率发送INFO命令,直到确认其转变为主节点
  3. 配置从节点
    1. 一旦确定新主节点,会向其它从节点发送SLAVEOF <ip> <port>命令
    2. 接收此命令的从节点解除和旧主节点的数据同步关系,开始从新主节点同步数据
    3. 监控从节点正确切换为和新主节点同步数据
  4. 降级旧主节点:如果旧主节点重新上线,向其发送SLAVEOF <ip> <port>命令,旧主节点降级为从节点并从新主节点同步数据
  5. 更新配置和通知
    1.通过发布/订阅机制向所有监听其频道的客户端和哨兵发送切换主节点消息,以方便客户端和其它哨兵更新主从信息
    2. 哨兵把主从节点信息持久化到自己的配置文件中,以方便重启后依然知道主从节点

7.2 集群高可用

集群模式理论高可用机器要求是至少3主3从:3主为满足选举的最小节点,3从则是每个主节点最少的备份节点

相比哨兵模式,其不仅支持高可用,还支持高扩展

核心概念

  • 集群心跳超时配置cluster-node-timeout,默认15秒
  • 心跳信息:会向集群内所有主从节点发送PING请求,所有节点收到后响应PONG,用于检测集群节点是否存活
    • 周期性随机选择每个节点每秒执行10次定时任务(每检测100ms一次),每次从集群列表挑选5个节点,并向该5个节点中最久没发送PING请求或最早收到PONG回复的节点发送一条PING消息
    • 智能补充检测每100ms扫描本地节点列表,若某节点最后一次收到PONG回复时间间隔超过心跳超时配置的一半,立即发送一条PING消息
  • 主观下线(PFAIL):若节点A向节点B发送了PING消息,节点B在心跳超时配置内未响应,则节点A标记节点B为PFAIL(Possible Failure)
    • 参与PFAIL标记的节点:所有主、从节点
    • 能被标记为PFAIL节点:所有主、从节点
  • 客观下线(FAIL):当节点A标记节点B为PFAIL,会向其它节点传播,若集群内其它节点在一定时间内都将节点B标记为PFAIL,则节点B被所有节点标记为FAIL
    • 参与FAIL标记的节点:所有主节点
    • 能被标记为FAIL节点:所有主、从节点
    • 被标记后果:主节点被标记为FAIL触发故障转移;从节点被标记减少其主节点副本冗余数量
    • 能否恢复正常:节点PING/PONG响应正常且是从节点
  • epoch:集群模式下有两种:
    • currentEpoch:集群全局选举轮次,每个节点都有,在故障转移发起选举和收到更大epoch时增加或更新,最终趋于一致
    • configEpoch:主节点独立维护,不同主节点值会不一样,在主节点槽所有权变更时修改为currentEpoch相同值

节点变动

  • 新增节点:执行CLUSTER MEET命令和集群内任一一个现有节点建立连接
    • 主节点:需要进行分片,将哈希槽从其他主节点迁移到新的主节点,需要借助工具
    • 从节点:指定进行数据同步的主节点,从主节点拉取同步数据
  • 删除节点:执行CLUSTER DEL-NODE删除节点
    • 主节点:需要完成槽迁移,使用redis-cli --cluster rebalance自动平哈希槽
    • 从节点:直接删除即可
  • 节点持久化:节点会使用Gossip协议自动拉取集群内所有节点信息,并持久化到配置文件中

从节点晋升主节点条件判断顺序:

  1. 筛选健康从节点:若从节点和主节点的断开时间过长,超过了cluster-node-timeout*cluster-slave-validity-factor阈值,将会失去参选资格
  2. 优先级:比较从节点的配置slave-priority值越小,优先级越高
  3. 复制偏移量:比较从节点的复制偏移量,偏移量越大,数据越新,优先级越高
  4. 选举轮次(epoch):以上条件相同,选举轮次越大,优先级越高
  5. 节点运行ID(Run ID):以上条件相同,运行ID越小,优先级越高

故障转移流程

  1. 从节点发起投票请求:当从节点对应的主节点下线后,从节点会判断自己是否有选举资格,有则将currentEpoch+1,并向集群所有主节点广播
  2. 主节点投票:使用晋升条件依次判断,投给最合适的从节点
  3. 投票裁决
    • 成功当选:从节点收到超过半数主节点的赞成票N/2+1
    • 投票失败:若一个epoch中无法诞生新主节点,重新进入下一轮投票
  4. 从节点晋升主节点:从节点执行SLAVEOF NO ONE命令,转变为主节点
  5. 接管哈希槽:将旧主节点的哈希槽指派给自己
  6. 广播通知状态更新:向集群广播一条PONG消息,其它节点接收到后会更新本地集群状态信息,消息内容:
    • 通知自己晋升为主节点
    • 负责的哈希槽映射关系
    • 当前主从最新的configEpoch
  7. 处理请求:最终开始处理自己负责的哈希槽相关指令
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值