《Redis 设计与实现》读书笔记
索引
1 复制
在 Redis 中,用户可以通过 slaveof 命令让一个服务器去复制另一个服务器,被复制的服务器为主服务器(master),对主服务器进行复制的服务器为从服务器(slave),主服务器和从服务器将保持相同的数据。
1.1 旧版复制功能
Redis 的复制功能分为同步(sync)和命令传播(command propagate)两个操作:
- 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态
- 命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主服务器的数据库重新回到一致状态
同步操作:
命令传播:同步操作后,客户端向主服务器发送的命令操作需要由主服务器向从服务器发送相同的命令
存在的问题:如果处于命令传播阶段的从服务器因为网络原因而中断了复制,则需要重连服务器后重头写入 RDB 文件,这是非常低效的。
1.2 新版复制功能
为了解决旧版复制的问题,Redis 2.8 之后使用 PSYNC 命令来替代 SYNC 命令执行复制时的同步操作。
PSYNC 命令具有完整重同步和部分重同步。
完整重同步跟上面一样,部分重同步算法如下所示:
部分重同步功能实现由三部分构成:主服务器的复制偏移量和从服务器的复制偏移量、主服务器的复制积压缓冲区和服务器的运行ID
1.3 复制功能实现
在复制操作刚开始的时候,从服务器会成为主服务器的客户端,并通过向主服务器发送命令请求来执行复制步骤,而在复制操作的后期,主从服务器会相互成为对方的客户端。
1.4 心跳检测
在命令传播阶段,从服务器默认会以每秒一次的频率,向主服务器发送命令:
REPLCONF ACK <replication_offset>
其主要作用为:
- 检测主从服务器的网络连接状态
- 辅助实现 min-slaves 选项
- 检测命令丢失
2 哨兵
哨兵是 Redis 的高可用解决方案:由一个或多个哨兵实例组成的哨兵系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器替代已下线的主服务器继续执行命令。
2.1 哨兵机制
下图展示了一个哨兵系统监视服务器的例子:
- 双环图案表示的是当前的主服务器 server1
- 单环图案表示的是三个从服务器
- 从服务器正在复制主服务器,而哨兵系统则正在监视所有四个服务器
下图显示,哨兵系统觉察到 server1 已经下线:
故障转移:
- 哨兵系统挑选 server1 属下的其中一个从服务器,并将这个被选中的从服务器升级为新的主服务器
- 哨兵系统会向 server1 属下的所有从服务器发送新的复制指令,让它们成为新的主服务器的从服务器,当所有从服务器都开始复制新的主服务器时,故障转移操作执行完毕。
- 哨兵系统还会继续监视已下线的 server1,并在它重新上线时,将它设置为新的主服务器的从服务器,如下两张图所示
2.2 故障转移原理及实现
启动并初始化哨兵(运行在特殊模式下的 Redis 服务器)redis-sentinel /path/to/your/sentinel.conf
- 初始化服务器
- 将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码
- 初始化 Sentinel 状态
- 根据给定的配置文件,初始化 Sentinel 的监视主服务器列表
- 创建连向主服务器的异步网络连接(命令连接和订阅连接,保证断线不丢失信息)
获取主服务器的信息
Sentinel 默认每十秒一次的频率,通过命令连接向被监视的主服务器发送 INFO 命令,并通过分析 INFO 命令的回复来获取主服务器本身的信息和主服务器所属从服务器的信息。
获取从服务器信息
当 Sentinel 发现主服务器有新的从服务器出现时,Sentinel 除了会为这个新的从服务器创建相应的实例结构之外,Sentinel 还会创建连接到从服务器的命令连接和订阅连接。
向主服务器和从服务器发送信息
在默认情况下,Sentinel 会以两秒一次的频率,通过命令连接所有被监视的主服务器和从服务器发送命令。而这个命令会向服务器的_sentinel_:hello
频道发送一条包含 Sentinel 本身和主服务器信息的信息。
接收来自主服务器和从服务器的频道信息
当 Sentinel 与一个主服务器或者从服务器建立订阅连接后,Sentienl 向服务器通过命令连接发送信息到频道,Sentinel 通过订阅连接从频道中接收来自服务器的信息
检测主观下线状态
Sentinel 会以每秒钟一次的频率向所有与它创建了命令连接的实例(主服务器、从服务器、其他Sentinel)发送 PING 命令,并通过实例返回的 PING 回复来判断实例是否在线。
判断客观下线状态
当 Sentinel 将一个主服务器判断为主观下线后,为了确认这个主服务器是否真的下线了,它会向同样监视这一主服务器的其他 Sentinel 进行询问,来确定下线状态。
选举领头 Sentinel
- 所有在线的 Sentinel 都有被选为领头 Sentinel 的资格
- 每次进行领头 Sentinel 选举后,不论是否选举成功,所有 Sentinel 的配置纪元(计数器)的值都会自增一次
- 在一个配置纪元里面,所有的 Sentinel 都有一次将某个 Sentinel 设置为局部领头 Sentinel 的机会,并且局部领头一旦设置,这个配置纪元里面就不能再更改
- 每个发现主服务器进入客观下线的 Sentinel 都会要求其他 Sentinel 将自己设置为局部领头 Sentinel
- Sentinel 设置局部领头 Sentinel 的规则是先到先得:最先向目标 Sentinel 发送设置要求的源 Sentinel 将称为目标 Sentinel 的局部领头 Sentinel,之后的所有设置要求都会被拒绝
- 如果某个 Sentinel 被半数以上的 Sentinel 设置成局部领头 Sentinel,那么这个 Sentinel 成为领头 Sentinel
选举新的主服务器
领头 Sentinel 会将已经下线的主服务的所有从服务器保存到一个列表里面,然后按照如下规则过滤选举:
- 删除列表中所有处于下线或者断线状态的从服务器
- 删除列表中所有最近五秒内没有回复过领头 Sentinel 的 INFO 命令的从服务器
- 删除所有与已下线主服务器连接断开超过 down-after-milliseconds * 10 的从服务器,down-after-milliseconds 为判断下线时间
- 之后按照最高优先级、复制偏移量最大,运行 ID 最小的从服务选为新的主服务器
3 集群
Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
3.1 节点
一个 Redis 集群通常由多个节点(node)组成,集群就是将各个节点相互连接起来。向一个节点 node 发送CLUSTER MEET <ip> <port>
命令,可以让 node 节点与 ip 和port 所指定的节点进行握手,当握手成功时,node 节点就会将 ip 和 port 所指定的节点添加到 node 节点当前所在的集群中。
每个节点都使用一个 clusterNode 结构来记录自己的状态(节点的创建时间,节点的名字,节点的当前配置纪元,节点的 IP 地址和端口号),并为集群中的所有其他节点(包括主节点和从节点)都创建一个相应的 clusterNode 结构,以此来记录其他节点的状态。
CLUSTER MEET 命令:
3.2 槽指派
Redis 集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为 16384 个槽(slot),数据库中的每个键都属于这 16384 个槽的其中一个,集群中的每个节点可以处理 0 个或最多 16384 个槽。当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态;相反,如果数据库中有任何一个槽没有得到处理,那么集群就处于下线状态。
一个节点除了会将自己负责处理的槽记录在 clusterNode 结构的 slots 属性和 numslots 属性之外,它还会将自己的 slots 数组通过消息发送给集群中的其他节点,以此来告知其他节点自己目前负责处理哪些槽。
3.3 在集群中执行命令
当客户端向节点发送与数据库键有关的命令时,接收命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:
- 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令
- 如果键所在的槽没有指派给当前节点,那么节点会向客户端返回一个 MOVED 错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令。
3.4 重新分片
Redis 集群的重新分片操作可以将任意数量已经指派给某个节点的槽改为指派给另一个节点,并且槽相关的键值对也会从源节点被移动到目标节点,Redis 集群的重新分片操作是由 Redis 的集群管理软件 redis-trib 负责执行的,具体过程如下:
3.5 ASK
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。
当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时:
- 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令
- 如果源节点没能在自己的数据库里面找到指定的键,那么这个键就可能被迁移到目标节点,源节点向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令。
3.6 复制和故障转移
Redis 集群中的节点分为主节点和从节点,其中主节点用于处理槽,而从节点则用于复制某个主节点,并在被复制的主节点下线时,替代下线主节点继续处理命令请求
下线节点的故障转移
- 复制下线节点的所有从节点,会有一个从节点被选中
- 被选中的从节点会执行 SLAVEOF no one 命令,成为新的主节点
- 先的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
- 新的主节点向集群广播一条 PONG 消息,这条 PONG 消息可以让集群中的其他节点立即知道这个节点已经由从节点变成主节点,并且这个节点已经接管了原本由已下线节点负责处理的槽
- 新的主节点开始接收和自己负责处理的槽有关的命令请求,故障转移完成
选举新的主节点
- 集群的配置纪元是一个自增计数器,初始值为 0
- 当集群中的某个节点开始一次故障转移操作时,集群配置纪元的值会被增1
- 对于每个配置纪元,集群里面每个负责处理槽的主节点都有一次投票机会,而第一个向主节点要求投票的从节点将会获得主节点的投票
- 当从节点发现自己正在复制的主节点进入已下线状态时,从节点会向集群广播一条消息,要求所有收到这条消息,并且具有投票权的主节点向这个从节点投票
- 如果一个主节点具有投票权,并且这个主节点尚未投票给其他从节点,那么主节点将会向要求投票的从节点返回一条消息,表示这个主节点支持从节点成为新的主节点
- 每个参与选举的从节点根据收到的消息统计自己获得多少主节点的支持
- 如果集群中有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 N/2+1 张支持票时,这个从节点就会被选为新的主节点
- 由于在每个配置纪元里,每个主节点只能投票一次,则保证了新的主节点只会有一个
- 如果此轮没有得到过半数的支持票,则集群进入一个新的配置纪元,再次选举
3.7 消息
集群中的各个节点通过发送消息和接受消息来进行通信,节点发送的消息主要有五种:MEET(加入集群)
、PING(在线检测)
、PONG(确认消息到达)
、FALL(节点下线)
、PUBLISH(执行相同指令)