第15章复制
在redis命令中,用户可以通过slaveof命令或者 slaveof 选项,让一个服务器去复制另一个服务器,我们称呼被复制的服务器为主服务(master)器,而对主服务器进复制的服务器则被成为从服务器(slave)
127.0.0.1:123456> slaveof 127.0.0.1 6379
进行复制中的主从服务器双方的数据库将保存相同的数据,概念上将这种现象称为“数据库状态一致”,或者简称”一致“
15.1旧版本复制功能的实现
复制功能分为sync同步和命令传播两个操作:
- 同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态
- 命令传播操作则作用域在祝福其的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,让主从服务器的数据库重新回到一致状态
15.1.1同步
步骤:
- 从服务器向主服务器发送sync命令
- 收到sync命令的主服务器执行bgsave命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有命令
- 当主服务器的bgsave命令执行完毕时,主服务器将bgsave命令生成的RDB文件发送欸从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态跟新至主服务器执行bgsave命令时的数据库状态
- 主服务器将激励在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库更新至主服务器数据库当前所处的状态。
15.1.2命令传播
在同步操作执行完毕之后,主从服务器两者的数据库将达到一致状态,但是这种一致并不是一成不变的,每当主服务器执行客户端发送的写命令时,主服务器的数据库就有可能被修改,并导致主从服务器状态不在一致。
15.2旧版复制功能的缺陷
复制分为初次复制和断线后重复制
初次复制旧版复制功能能够很好地完成任务,但对断线后重复制,效率特别低。
15.3新版复制功能地实现
从redis2.8开始使用psync命令代替sync命令来执行复制时地同步操作。
psync命令具有完整重同步和部分冲同步两种模式:
完整重同步用于处理初次复制情况
部分重同步用于处理断线后重复制情况,同于处理短线后重复制情况
15.4部分重同步地实现
由以下三部分构成:
- 主服务器地复制偏移量和从服务器的复制偏移量
- 主服务器的复制积压缓冲区
- 服务器的运行ID
15.4.1复制偏移量
主从服务器分别维护一个复制偏移量:
主服务器每次向从服务器传播N个字节是的数据时,旧将自己的复制偏移量的值加上N
从服务器每次收到主服务器传播来的N字节数据时,就将自己的复制偏移量的值加上N。
15.4.2复制积压缓冲区
复制积压缓冲区由主服务器维护的一个固定长度,先进先出队列,默认大小为1MB
当服务器重新连上主服务器时,从服务器会通过PSYNC命令将自己的复制偏移量offset发送给主服务器,主服务器会根据这个复制偏移量来决定对从服务器执行何种同步操作。
可以根据需要调整复制积压缓冲区的大小,可以根据公式second*write_size_per_second来估算。second为从服务器断线后重新连上主服务器所需的平均事件(以秒计算)
15.4.3服务器运行ID
每个redis服务器,不论主服务器还是从服务器,都有自己的运行ID,每个ID在服务器启动时自动生成,由40个十六进制字符组成。
初次复制时,主服务器将自己的运行ID传送给从服务器,而从服务器则会将这个运行ID保存起来。
15.5PSYNC 命令的实现
PSYNC命令调用方法有两种:
- 从服务器以前没有复制过任何主服务器,或者之前执行过slaveof no one命令,那么从服务器在开始一次新的复制时将向主服务器发送PSYNC ?-1命令,做的请求主服务器进行完整重同步。
- 从服务器在开始一次新的复制时,将向主服务器发送PSYNC命令,其中runid时上一次复制的主服务器的运行ID,offset时从服务器当前的复制偏移量,接收到这个命令的主服务器会通过这两个参数来判断对从服务器执行哪种同步操作。
15.6复制的实现
- 设置主服务器的地址和端口 slaveof master_ip master_port
- 建立套接字
- 发送ping命令,返回pong
- 身份验证
若设置了masterauth选项,则需要进行身份验证
向主服务器发送:auth命令
- 发送端口信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QAylHka1-1646304245693)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129084921357.png)]
向主服务器发送 replconf listen-port 12345
- 同步
从服务器向主服务器发送psync命令,执行同步操作,并将自己的数据库更新至主服务器数据库当前状态。
- 命令传播
同步之后,主从服务器就会进入命令传播阶段,主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器一直接收并执行主服务器发来的写命令。
15.7心跳检测
每秒一次的频率,向主服务器发送命令:
replconf ack replication_offset
- 检测主从服务器的网络连接状态
- 辅助实现min-slave选项
- 检测命令丢失
15.7.1检测主从服务器的网络连接状态
主从服务器可以通过发送replconf ack命令来检查两者之间的网络连接是否正常;
命令:info replication命令,可以看到相应从服务器最后一次向主服务器发送replconf ack命令距离现在过了多少秒
15.7.2辅助实现min-slaves
redis的min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令
min-slaves-to-write 3
min-slaves-max-lag 10
3个从服务器的延迟值大于或等于10吗
15.7.3检测命令丢失
如果出现网络故障,主服务器传播给从服务器的写命令在半路丢失,那么当从服务器向主服务器发送repconf ack命令是,主服务器将发觉从服务器当前的复制偏移量少于自己的复制偏移量,然后主服务器根据从服务器提交的复制偏移量,在复制积压缓冲区里找到从服务器缺少的数据。
第16章 Sentinel
Sentinel(哨兵、哨岗)是redis高可用性解决方案:由一个或多个Sentinel组成的Sentinel系统可以监视任意多个主服务器。
自动下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器待敌已下线的主服务器继续处理命令请求。
16.1启动并初始化Sentinel
启动Sentinel命令:
redis-sentinel /path/your/sentinel.conf
执行以下步骤
16.1.1初始化服务器
Sentinel本质上只是一个运行在特殊模式下的Redis服务器。启动第一步,与第14章类似
可能会用到的主要功能
- 复制命令SLAVEOF:Sentinel内部使用,客户端不可以使用
- 发布与订阅命令:SUBSCRIBE、PSUBSCRIBE、UNSUBSCRIBE、PUNSUNSCRIBE在sentinel内部和客户端可以使用,但是PUBLISH只能在sentinel内部使用
- 文件事件处理器:Sentinel内部使用,关联的文件事件处理器和普通的Redis服务器不同
- 时间事件处理器:serverCron会调用sentinelTimer(desynchronize every Sentinel,确保可以选出leader)
16.1.2使用Sentinel专用代码
将一部分普通Redis服务器使用代码替换成Sentinel专用代码
- 端口
- 普通Redis服务器使用的6379
- sentinel使用的:#define REDIS_SENTINEL_PORT 26379
16.1.3初始化Sentinel状态
-
服务器一般状态还使用redisServer,sentinelState状态保存服务器中和sentinel相关的状态
-
成员变量部分介绍
current_epoch:当前纪元,用来实现故障转移
masters:所有被sentinel监视的主服务器,字典类型,key是主服务器的名字,value是指向sentinelRedisInstance指针
16.1.4初始化Sentinel状态的masters属性
Sentinel状态中的masters字典中记录了所有被Sentinel监视的主服务器的相关信息,包括,
- 字典的键是被监视主服务器的名字
- 字典的值是被监视主服务器对应sentinelRedisInstance结构
16.1.5创建连向主服务器的网络连接
初始化Sentinel的最后一步创建连向监视主服务器的网络连接,Sentinel将成为主服务器的客户端,它可以香主服务器发送命令,并从命令回复中获取相关信息。
对于每个被Sentinel监视的主服务器来说。Sentinel会创建两个连向主服务器的异步网络连接:
- 命令连接:用于向主服务器发送命令
- 订阅连接:用于订阅主服务器_sentinel= hello频道
16.2获取主服务器信息
Sentinel默认每十秒一次的频率,通过命令连接向监视的主服务器发送INFO命令,并通过分析INFO命令的回复来获取主服务器的当前信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4dJtP6F-1646304245695)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129140119074.png)]
主要包含两方面信息:
- 关于主服务器本身信息,运行id,服务器role角色等
- 关于主服务器属下所有从服务器的信息
16.3获取从服务器信息
sentinel发现主服务器有从服务器,会创建对应的实例结构,还会创建连接到从服务器的命令连接和订阅连接
sentinel默认情况下,会每十秒通过命令连接向从服务器发送INFO命令,获取以下信息:
- 从服务器的run_id
- 从服务器的role
- 主服务器的ip地址和端口
- 主从服务器的连接状态
- 主从服务器的优先级
- 从服务器的复制偏移
16.4向主服务器和从服务器发送信息
默认情况下,Sentinel会以每两秒一次的频率,向所有被监视的主服务器和从服务器发送以下格式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KVOcvczL-1646304245695)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129140815992.png)]
这条命令向服务器_Sentinel:hello频道里发送了一条信息,包含:
- s_开头的sentinel本身的信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XcLSvac-1646304245695)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129141210174.png)]
- m_开头主服务器信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IWveuJ7j-1646304245696)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129141219659.png)]
16.5接收来自主服务器和从服务器频道信息
当Sentinel与一个主服务器或者从服务器建立起订阅连接后,Sentinel就会通过订阅连接,向服务器发送以下命令:
subcribe _sentinel:hello
当Sentinel从_sentinel:hello频道收到一条信息时,Sentinel会对这条信息进行分析,提取16.4提到参数,进行检查
16.5.1更新sentinels字典
- sentinel可以分析接收到的频道信息知道其他sentinel的存在(每两秒sentinel会发给服务器)
16.5.2 创建连接到其他sentinel的命令连接
- sentinel通过频道发现监视同一服务器的其他sentinel,两个sentinel之间会形成连接(只有命令连接用来通信,没有订阅连接),最后监视同一主服务器的多个sentinel将形成相互连接的网络
16.6检测主观下线状态
默认情况,Sentinel每秒一次的频率向它创建目录连接的实例(包括主服务器、从服务器、其他Sentinel在内)发送Ping命令,并通过返回ping回复判断实例是否存在。
有效回复:pong、loading、masterdown
无效回复:除以上,均无效
配置文件在,down-after-milliseconds选项指定了Sentinel判断进入主观下线所需的时间长度,默认50000,表明5000毫秒内无回复,任务主观下线。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S1kxDAHT-1646304245697)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129142728326.png)]
16.7检查客观下线
当Sentinel从其他Sentinel接收到足够数量的已下线判断,Sentinel将从服务器怕你定位客观下线,并对主服务器执行故障转移操作
16.7.1发送 sentinel is-master-down-by-addr命令
Sentinel使用 is-master-down-by-addr 询问其他Sentinel是否同意主服务器已下线
16.7.2接收sentinel is-master-down-by-addr命令
Multi Bluk作为sentinel is-master-down-by-addr命令的回复
- down_state
- leader_runid
- leader_epoch
16.7.3接收sentinel is-master-down-by-addr命令回复
- 据其他sentinel的回复,sentinel会统计同意下线的数量,如果大于等于quorum,可以判断客端下线,sentinel会将主服务器实例结构的flags的SRI_O_DOWN标识打开
- 多个sentinel设置的quorum可能不同,所以客观下线可能不同
16.8选举领头Sentinel
当一个主服务器被判断为客观下线,监控下线主服务器的sentinel会进行协商,选举一个领头的sentinel,领头的sentinel对下线主服务器进行故障转移
如何选举:
- 所有的sentinel都有可能成为领头
- 每次领头选举之后,不论选举是否成功,sentinel的配置纪元都会自增一次
- 局部领头被设置之后,配置纪元就不能再修改
- 每个发现主服务器进入客观下线的sentinel,会要求其他sentinel把自己设置成局部领头
- 源sentinel发给目标sentinel的is-master-down-by-addr表示:源要求目标把源设置成目标的局部领头sentinel
- 目标sentinel的局部领头,哪个源先发过来就设置成哪个(先到先得)
- 目标收到sentinel is-master-down-by-addr,会向sentinel返回一条命令回复,回复中leader_runid和leader_epoch分别记录了目标sentinel的局部领头的信息
- 源收到目标sentinel,如果leader_epoch和自己的配置纪元相同,取出leader_runid。如果leader_runid和自己的runid一致,表明源就是leader
- 如果某个sentinel被半数以上的sentinel设置成局部领头,它就变成真正的领头sentinel
- 每个配置纪元只能设置一次局部领头,需要得到半数以上的支持,所以在配置纪元里面只会出现一个领头sentinel
- 如果给定时间没有选出来,那么sentinel会在一段时间之后再次进行选举,直到选择出来
16.9 故障转移
选出领头sentinel之后,领头sentinel会对已下线的主服务器执行故障转移操作,有三步
- 从已下线的主服务器中选择一个从服务器,将其转换成主服务器
- 已下线主服务器下面的从服务器,修改成复制新的主服务器
- 将已下线的主服务器设置成新服务器的从服务器。如果旧的主服务器重新上线,就会变成新的主服务器的从服务器
16.9.1 选出新的主服务器
选择一个状态良好、数据完整的从服务器,向这个从服务器发送SLAVE no one命令,将它变成主服务器
怎么选择
- 把主服务器的所有从服务器保存到一个列表中
- 删除下线或者短线的服务器
- 删除最近5s内没有回复领头的INFO命令的从服务器
- 删除所有与已下线主服务器连接断开超过10毫秒的服务器,留下来的数据比较新
- 根据从服务器的优先级,对列表中的进行排序,选出优先级最高的从服务器(根据复制偏移量选择最大的,运行ID最小的)
16.9.2 修改从服务器的复制目标
server1下线,server2变成主,server3和server4是从领头sentinel发送命令给server2和server3:SLAVEOF SERVER2IP SERVER2PORT
16.9.3 将旧的主服务器变成从服务器
server1上线,领头sentinel向server1发送SLAVEOF命令,让它变成server2的从服务器
第17章集群
集群是Redis提供的分布式数据库方案,集群通过分片来进行数据共享,并对复制和故障转移。
17.1节点
一个集群通常多个节点组成,开始时相互独立,处于一个只包含自己的集群中
连接各个节点的工作使用 CLUSTER MEET 命令来完成:
CLUSTER MEET <ip> <port>
17.1.1启动节点
Redis会根据cluster-enabled配置选项是否为yes来决定开启服务器的集群模式。
点会继续使用单机模式下使用的服务器组件,如文件事件处理器、时间事件处理器、数据库、RDB 和 AOF 持久化、发布与订阅、复制模块、Lua 脚本执行等
还好继续使用 redisServer 结构来保存服务器的状态,使用 redisClient 来保存客户端的状态,而集群模式下使用的数据,将保存在 cluster.h/clusterNode、clusterLink、clusterState 结构中
17.1.2集群数据结构
每个节点都会使用一个clusterNode结构来记录自己的状态,并为集群中的所有其他节点都船舰一个相应的clusterNode结构,以此来就其他节点的状态:
创建时间、名字、配置纪元、IP、端口号等
link属性是clusterLink结构,保存了节点所需的有关信息
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qeXfvTv8-1646304245698)(https://cdn.jsdelivr.net/gh/ZephXu07/newimages@master/20210714/clusterLink.3kbr07jrqn20.png)]
redisClient 结构和 clusterLink 结构相似,区别在于 redisClient 是连接客户端的,而 clusterLink 是连接节点的
每个节点都保存着 clusterState 结构,记录了当前节点的视角下,集群目前所处的状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9T8s7n3W-1646304245698)(https://cdn.jsdelivr.net/gh/ZephXu07/newimages@master/20210714/clusterState.54768006lzo0.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7HO4iPXU-1646304245698)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129151143701.png)]
17.1.3cluster meet 命令实现
向 A 发送此命令,将 B 连接
- A 为 B 创建 clusterNode 结构,添加到 clusterState.nodes 字典里
- A 根据 IP、端口,向 B 发送 MEET 信息
- 正常情况下 B 收到 A 的信息,为 A 创建 clusterNode 结构,添加到自己的 clusterState.nodes 字典中
- B 向 A 返回 PONG 信息
- 正常情况 A 收到 B 的 PONG 信息,知道 B 已经正常接收 MEET 信息
- A 向 B 发送 PING 信息
- 正常情况 B 收到 PING 信息,B 通过此信息知道 A 收到 PONG 信息,握手完成
- A 通过 Gossip 协议传播给集群其他的节点,让其他节点也与 B 握手,最后 B 被集群中所有节点认识
17.2槽指派
Redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分到16384个槽。数据库的每个键都属于u这个16384个槽的其中一个,每个节点最多可以出现16384个槽。
通过向节点发送Cluster addslots命令,可以将一个或多个槽指派给节点
clusteraddslots <slot>{slot...}
17.2.1记录节点的槽指派信息
clusterNode结构的slots属性和numslot属性记录了节点负责哪些槽
slots 为一个二进制位数组,长度为 16834 / 8 = 2048 个字节
0 为起始索引,16383 为终止索引,如果此索引上二进制位为 1 ,表示由此节点负责,否则不是
17.2.2传播节点的槽指派信息
节点不仅记录自己的槽指派信息,同时也会传播告知其他节点自己目前负责处理的槽
其他节点收到信息后会在自己的 clusterState.nodes 字典查找节点对应的 clusterNode 结构,并对结构中的 slots 数组进行保存
17.2.3记录集群所有槽的指派信息
clusterState 结构中的 slots 数组记录了集群中所有槽的指派信息
包含 16384 个项,每个数组项都是指向一个 clusterNode 结构的指针:
- 指向 NULL:槽 i 没有指派给任何节点
- 指向 clusterNode 结构:此结构代表的节点
17.3在集群中执行命令
在对数据库中的16384个槽都进行了指派之后,集群就会进入上线状态,这是客户端可以向集群中的节点发送数据命令了。
客户端向节点发送与数据库有关的命令时,接收命令的节点会计算出命令要处理的数据库属于哪个键,并检查这个槽是否指派给了自己
- 槽属于当前节点,直接执行
- 槽不属于当前节点,节点向客户端返回一个 MOVED 错误,指引客户端转向至正确的节点,并再次发送之前想要执行的命令
17.3.1计算键书属于哪个键
使用
cluster keyslot<key> 可以查看一个给定键属于哪个槽
17.3.2判断是否由当前节点负责处理
计算出槽 i 之后,节点检查自己在 clusterState.slots 数组的项 i ,判断是否自己负责
- clusterState.slots[i] = clusterState.myself,当前节点负责,执行客户端发送的命令
- 否则根据 clusterState.slots[i] 指向的 clusterNode 结构记录的节点 IP 和端口号,向客户端返回 MOVED 错误,指引客户端转向至处理槽 i 的节点上
17.3.3moved错误
当节点发现键所在的槽并非由自己复制处理的时候,节点就会像客户端返回一个moved错误,指引客户端转向负责槽的节点。
格式为:
MOVED <slot> <ip>:<port>
一个集群客户端通常会与多个节点创建套接字连接,所谓的节点转向实际上是换一个套接字来发送命令。
如果尚未创建与即将转向的节点的套接字则会根据 MOVED 错误提供的 IP 和端口号来连接节点再进行转向。
集群模式下 MOVED 错误不会被打印出来,客户端根据此错误自动转向并打印转向信息。
17.3.4节点数据库的实现
集群节点保存键值对以及键值对过期时间的方式与单机服务器一致
区别是集群的节点只能使用 0 号数据库,而单机服务器没有限制
节点会用 clusterState 结构中的 slots_to_keys 跳跃表来保存槽和键之间的关系
slots_to_keys 跳跃表每个节点的分值是一个槽号,每个节点的成员是一个数据库键:
- 当节点往数据库添加新的键值对时,节点会将键以及键的槽号关联到 slots_to_keys 跳跃表
- 删除某个键值对时,节点会在 slots_to_keys 跳跃表接触键与槽的关联
通过 slots_to_keys 跳跃表,节点可以很方便对属于某个或某些槽的数据库键进行批量操作
CLUSTER GETKEYSINSLOT <slot> <count>
返回最多 count 个属于槽 slot 的数据库键
17.4重新分片
重新分片:将任意数量已经指派给某个节点的槽改为指派给另外一个节点,且相关槽所属的键值对也会从源节点被移动到目标节点
可以在线进行,集群不需要下线,源节点和目标节点都可以继续处理命令请求
分片原理
redis-trib 负责执行的,Redis 提供重新分片的所有命令,redis-trib 通过向源节点和目标节点发送命令进行分片:
- redis-trib 对目标发送
CLUSTER SETSLOT <slot> IMPORTING <source_id>
,让目标节点准备好从源节点导入属于槽 slot 的键值对 - redis-tirb 对源发送
CLUSTER SETSLOT <slot> MIGRATING <target_id>
命令,让源节点准备好将属于槽 slot 的键值对迁移到目标节点 - redis-trib 向源发送
CLUSTER GETKEYSINSLOT <slot> <count>
命令,获得最多 count 个属于槽 slot 的键值对的键名 - 对于 3 获得的键名,redis-trib 都向源发送一个
MIGRATE <target_ip> <target_port> <key_name> O <timeout>
命令,将被选择的键原子地从源迁移至目标 - 重复 3 4 ,直到源保存的所有属于槽 slot 的键值对都被迁移到目标节点为止
- redis-trib 向集群中任意一个节点发送
CLUSTER SETSLOT <slot> NODE <target_id>
命令,将槽 slot 指派给目标节点,这信息将通过消息发送整个集群,最后集群所有节点都会知道槽 slot 已经指派给目标节点
17.5ASK错误
ask错误过程
17.5.1 cluster setslot importing命令实现
clusterState 结构的 importing_slots_from 记录了当前节点正在从其他节点导入的槽
importing_slots_from[i] 不为 NULL,而是指向一个 clusterNode 结构,则表示当前节点正在从 clusterNode 代表的节点导入槽 i
重新分片,向目标节点发送命令:
CLUSTER SETSLOT IMPORTING <source_id>
将目标节点的 clusterState.importing_slots_from[i] 设置为 source_id 代表的 clusterNode 结构
17.5.2 CLUSTER SETSLOT MIGRATING 命令的实现
*clusterState 结构的 migreting_slots_to 记录了当前节点正在迁移至其他节点的槽migreting_slots_to[i] 不为 NULL,而是指向一个 clusterNode 结构,则表示当前节点正在向 clusterNode 代表的节点迁移槽 i重新分片,向源节点发送命令:CLUSTER SETSLOT *MIGRATING <target_id>将源节点 clusterState.migrating_slots_to[i] 的值设置为 target_id
17.5.3ASK错误
17.5.4ASKING命令
唯一要做的是打开发送该命令的客户端的 REDIS_ASKING 表示
17.5.5ASK与MOVED错误的区别
- MOVED 代表槽的负责权已经从一个节点转移到另外一个节点
- 客户端收到槽 i 的 MOVED 错误后,客户端每次遇到 槽 i 的命令请求时,都直接将命令请求发送至 MOVED 错误指向的节点
- ASK 错误是两个节点迁移槽过程使用的临时措施
- 客户端收到槽 i 的ASK 错误后,客户端只会在接下来一次的命令请求关于槽 i 的命令发送至 ASK 错误指示的节点,但这种转向只是一次性的,后续的仍会发送至当前的节点,除非 ASK 错误再次出现
17.6复制与故障转移
节点分为主节点和从节点,主节点处理槽,从节点复制某个主节点,并在被复制的主节点下线时代替下线主节点继续处理命令请求
17.6.1设置从节点
向一个节点发送命令:
CLUSTER REPLICATE <node_id>
让接收命令的节点成为 node_id 指定的节点的从节点,对其进行复制
- 节点在自己的 clusterState.nodes 字典中找到 node_id 对应节点的 clusterNode 结构,并将自己的 clutserState.myself.slaveof 指针指向此结构,记录本节点只在复制的主节点
- 节点修改自己在 clusterState.myself.flags 的属性,关闭原来的 REDIS_NODE_MASTER 标识,打开 REDIS_NODE_SLAVE 标识,表示成为从节点
- 最后节点调用复制代码,根据指向的 clusterNode 结构保存的 IP 地址和端口进行复制,与单机 Redis 复制功能使用相同代码
17.6.2故障检测
集群中每个节点会定期地向其他节点发送 PING 消息,来检测对方是否在线,如果没有在规定的时间内返回 PONG 消息,则会被标记疑似下线
当主节点 A 通过消息得知主节点 B 认为主节点 C 进入了疑似下线状态,则会在自己的 clusterState.nodes 字典中找到 C 对应的 clusterNode 结构,并将 B 的下线报告添加到 clusterNode 结构的 fail_reports 链表里面
每个下线报告由一个 clusterNodeFailReport 结构表示
在一个集群中,半数主节点都将某个主节点 x 报告为疑似下线,则 x 将被标记为已下线,再广播出去,所有收到此广播的主节点便将 x 标记为已下线
17.6.3 故障转移
当一个主节点进入已下线状态,从从节点对其进行故障转移:
- 复制下线主节点的所有从节点里有一个被选中
- 选择从节点执行 SLAVEOF no one,成为新的主节点
- 新主节点撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
- 新主节点向集群广播 PONG 消息,让其他主节点知道其已经接替成为新的主节点
- 新主节点接收和自己负责处理的槽的有关命令的请求,故障转移完成
17.6.4 选举新的主节点
- 集群配置纪元是自增计数器,初始值为 0
- 某个节点开始一次故障转移,则配置纪元的值增 1
- 每个配置纪元中,正在处理槽的主节点有一次投票机会,第一个向主节点要求投票的从节点将获得其的投票
- 从节点发现自己正在复制的主节点进入已下线状态,从节点会向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票
- 在处理槽的主节点尚未投票给其他从节点,则向要求投票的从节点返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 信息,表示这个主节点支持从节点成为新的主节点
- 每个参与选举的从节点接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 信息并统计自己获得多少主节点的支持
- 集群 N 个主节点,当一个从节点收集到大于等于 N / 2 + 1 支持票时,从节点就会当选成为新的主节点
- 因为在每一个配置纪元中,每个具有投票权的主节点只能投一次票,当有 N 张票时,大于等于 N / 2 + 1 的票的从节点只有一个,即新的主节点只有一个
- 如果在一个配置纪元中没有一个从节点能获得 N / 2 + 1 的支持票,则集群进入下一个配置纪元,再次进行选举,直到选出新的主节点为止
17.7 消息
种类
- MEET :发送者发送此消息请求接收者加入发送者所处的集群
- PING:每秒从已知列表选出五个节点,对其中最长时间没有发送 PING 消息的节点发送 PING 消息,检查节点是否在线;节点 A 最后一次收到节点 B 发送的 PONG 消息超过设置的 cluster-node-timeout 选项设置的时间的一半,则 A 也会向 B 发送 PING 消息,防止随机时挑选不到而导致 B 的消息滞后
- PONG:接收者收到 MEET 或 PING 消息时,向发送者确认消息已到达;通过集群广播自己的 PONG 消息来让集群其他节点立即刷新关于节点的认识
- FAIL:一个主节点判断另外一个主节点进入 FAIL 状态时进行广播,所有收到此消息的节点会将 FAIL 状态节点标记已下线
- PUBLISH:一个节点收到 PUBLISH 命令,执行后广播一条 PUBLISH 消息,所有收到的节点执行相同的操作
消息由消息头和消息正文组成
17.7.1 消息头
消息头包裹在消息外,还记录了发送者部分信息由 cluster.h/clusterMsg 表示:
clusterMsg.data 指向 cluster.h/clusterMsgData 消息正文:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vIi2iAfd-1646304245701)(https://cdn.jsdelivr.net/gh/ZephXu07/newimages@master/20210717/clusterMsgData.6olaxkjh3mw0.png)]
17.7.2 MEET、PING、PONG 消息的实现
通过 Gossip 协议交换信息,使用这三种实现,都由两个 cluster.h/clusterMsgDataGossip 结构组成
因为都是使用相同的消息正文,所以节点通过消息头的 Type 属性来判断一条消息是哪种类型每次发送消息时,发送者从自己已知节点列表中随机取出两个节点(主或从),保存在两个 clusterMsgDataGossip 结构中,记录了被选中节点名字,发送者与被选择节点最后一次发送和接收 PING 消息的时间戳、被选中节点 IP、端口,
接收者收到消息后访问这两个结构,根据是否认识其中被选中节点进行操作:
- 不存在已知节点列表:第一次接收到,根据 IP、端口号进行握手
- 存在已知节点列表:更新对应的 clusterNode 结构
17.7.3 FAIL 消息的实现
节点数量比较大时,单纯使用 Gossip 协议来传播节点的已下线消息会带来延迟,因为 Gossip 协议需要一段时间才能通知整个集群,而发送 FAIL 消息可以让整个集群都知道某个主节点已下线,从而尽快判断是否将集群标记为已下线,又或者对下线节点进行故障转移
FAIL 消息的正文由 cluter.h/clusterMsgDataFail 结构表示,其只包含一个 nodename 属性,记录了已下线节点的名字
集群中节点名字是唯一的,所以 FAIL 消息只需要保存下线节点的名字,接收节点即需要根据这个名字则可以判断
17.7.4 PUBLISH 消息的实现
客户端向集群中某个节点发送消息:
接受者不仅会向 channel 发送 message,还好向集群广播 PUBLISH 消息,再接收者重复,即向集群中某个节点发送此命令都会导致集群中所有节点向 channel 频道发送 message 消息
PUBLISH 消息正文由 cluster.h/clusterMsgDataPublish 结构表示
bulk_data 属性是字节数组,保存了客户端通过 PUBLISH 命令发送给节点的 channel、message 参数,channel_len、message_len 则保存了长度
- 0 - channel_len 字节是 channel 参数
- channel_len + messgae_len - 1 字节是 message 参数
第18章发布与订阅
此功能由PUBLISH(向某频道发送)、SUBSCRIBE(订阅频道)、PSUBSCRIBE等命令组成
SUBSCRIBE和PUBLISH:
eg:
ABC都执行了 SUBSCRIBE “news.it”,三个客户端都是这个频道的订阅者。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zILdcRO0-1646304245702)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129163021823.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3bgLCmNb-1646304245702)(C:\Users\崔常菲\AppData\Roaming\Typora\typora-user-images\image-20211129163037168.png)]
18.1频道的订阅与推动
当客户端执行subscribe命令订阅某个或某些频道的时候,这个客户端与被订阅频道之间就建立了一种订阅关系。
18.1.1订阅频道
客户端执行SUBSCRIBE命令时,服务器会将客户端与被订阅的频道在pubsub_channels字典中进行关联
根据频道是否已经有其他订阅者,关联操作分为两种情况:
- 如果频道已经由其他订阅者,那么它在pubsub_channels字典中必然由相应的订阅者链表,程序,程序唯一要做的就是将客户端添加到订阅者链表的末尾。
- 如果频道还未由任何订阅者,那么他在pubsub_channels字典,程序首先在pubsub_channels字典中为频道建立一个键,并将这个键设置为空链表.
18.1.2 退订频道
客户端执行UNSUBSCRIBE命令时,服务器会从pubsub_channels字典中解除客户端与被订阅的频道之间的关联:
- 程序会根据被退订频道的名字,在pubsub_channels字典中找到频道对应的订阅者链表,然后从订阅者链表在删除推动客户端信息
- 如果删除退订客户端之后,频道订阅者链表变成了空链表,那么说明这个频道已经没有任何订阅者了,程序从pubsub_channels字典中删除对应的键
18.2模式的订阅和退订
模式的订阅关系保存在服务器状态的pubsub_patterns链表属性里面:
pubsub_patterns链表的每个节点都包含一个pubsubPattern结构,这个结构的pattenr属性记录了被订阅的模式,client属性记录了订阅模式的客户端:
18.2.1 订阅模式
客户端执行PSUBSCRIBE命令时,服务器会对每个被订阅的模式执行以下两个操作:
18.2.2 退订模式
客户端执行PUNSUBSCRIBE命令时,服务器会从pubsub_patterns链表中查找并删除那些pattern属性为输入的退订模式,并且client属性为执行退订命令的客户端的pubsubPattern结构
18.3发送消息
客户端执行
PUBLISH < channel > < message >
命令时服务器需要执行以下两个动作:
1.将消息message发送给channel频道的所有订阅者
2.如果有一个或多个模式pattern与频道channel相匹配,那么将消息message发送给pattern模式的订阅者
18.3.1 将消息发送给频道订阅者
在pubsub_channels字典里找到指定频道channel的订阅者名单链表,然后发送消息给这个链表上的所有客户端即可
18.3.2 将消息发送给模式的订阅者
遍历pubsub_patterns链表,查找那些与指定channel相匹配的模式(通过pubsubPattern的pattern属性),然后发送消息(通过pubsubPattern的client属性)