集群通过分片(sharding)来进行数据共享,并提供复制和故障转移功能。
1.节点
一个节点就是一个运行在集群模式下的Redis服务器。启动Redis服务器时,通过判断cluster-enabled选项,选择是否开启集群模式。(Yes开启集群,No则单机模式普通服务器)
一个Redis集群由多个节点组成,每个节点使用的端口各不相同,可以设置。每个节点最开始可以看做一个只有自己节点的集群,节点间通过命令相互握手,组建集群
握手命令
cluster meet 127.0.0.1 7001 //与ip为127.0.0.1,端口为 7001的节点握手 cluster nodes //显示当前集群的节点信息
2.集群数据结构
clusterState 和 clusterNode 以及 slots
每个节点都有 clusterState结构。
clusterState结构的里面包含 :
slots[16384]数组,
myself属性,(指向自己对应的clusterNode,直接通过该属性访问自己对应的clusterNode,这样更快、方便各种自己节点信息的更新操作)
nodes属性,
slots_to_key跳跃表,
importing_slots_from[16384]数组(记录当前节点从其他节点导入的槽,为null未导入,指向clusterNode则为从该clusterNode对应的节点导入槽i)
migrateing_slots_to[16384]数组(记录当前节点迁移到其他节点的槽,对应槽索引的值指向目标节点对应的clusterNode)
每个节点都有一个对应的clusterNode结构。
clusterNode中包含:
slots数组
numslot属性
slaveof属性---指向正在复制的主节点的对应clusterNode
flags属性---REDIS_NODE_MASTER 表示该节点是主节点
REDIS_NODE_SLAVE 表示该节点时从节点
numslaves---复制该节点的从节点数量
slaves数组---每个元素都指向该节点下属从节点的clusterNode
faile_reports链表---记录其他节点对该节点的下线报告(在线、疑似下线PFAIL、已下线FAIL)
nodes字典记录每个节点与clusterNode的对应关系
myself指向属于该节点对应的clusterNode
例:设集群中目前有7000、7001、7002三个节点。对于7000端口的节点,其拥有一个clusterState结构,三个clusterNode结构(分别对应三个节点),myself属性指向属于自己的那个clusterNode节点。
3.槽指派
Redis集群通过分片的方式来保存数据库中的键值对;集群的整个数据库被分为16384个槽(索引为0~16383);数据库中的每个键都属于槽中的一个。每个节点都最多处理0~16384个槽。只有这16384个槽都被分配到节点时,Redis集群才处于上线状态(ok);否则,只要有任意一个未分配,则集群处于下线状态(fail)。
槽分配命令
127.0.0.1:7000> cluster addslots 0 1 2 3 ... 5000 //将0~5000的槽分配个7000节点
节点的 clusterState中的 slots[16384]数组,记载着集群中所有槽的指派信息,即槽是否被指派?被分配给了哪个节点?
节点的 clusterNode中的 slots数组则使用0-1标记法来表示,该节点处理的槽;处理,则对应槽索引值为1。使用 numslot属性记录该节点处理的槽总数
每个节点除了在自己对应的clusterNode中保存自己的槽分配信息外,还会将自己的slots数组通过消息发送给其他节点,让其他节点保存。即对于7000节点而言,剩下的两个clusterNode也会根据收到的其他节点槽相关信息,来更新clusterNode的属性。(收到消息后,先通过clusterState.nodes查询对应节点的clusterNode,再对其中的slots数组保存更新)
为什么同时使用clusterNode中的slots和clusterState的slots?
这是典型的以空间换时间。
1. 通过clusterState中的slots[]来查询槽点是否被指派和指派节点的复杂度为 O(1);避免了通过nodes字典的映射去依次遍历clusterNode中的slots[]
2.clusterNode中的slots[]可以用于作节点间互相发送槽信息。这样就直接发送自己对应的clusterNode.slots[]即可,无需对clusterState的slots[]进行遍历来找到自己负责了哪些槽了
总结:clusterState.slots[]数组记录了集群中所有槽的指派信息;clusterNode.slots[]数组记录了clusterNode结构所代表节点的槽指派信息。
4.节点数据库
集群节点保存键值对以及键值对过期方式与单机数据库一样。
节点与单机服务器数据库方面的一个区别就是:节点只能使用0号数据库,即db[0](默认服务器启动时,初始化16个数据库)
clusterState的slots_to_key跳跃表保存 槽 和 键 之间的关系 (注意,每个节点的clusterState的跳跃表,只保存属于自己节点处理的 槽 和 键 的对应关系)
分值(score)对应 槽号;成员(member)对应 数据库键对象
用跳跃表保存,方便对某个或某些槽的所有数据键进行批量操作
5.重新分片
概念:将任意数量已经指派给某个节点的槽,改为指派给另一个节点,其中,槽对应的所有键值对都需要迁移。
特点:1、重新分片是可以在线进行的,集群无需下线
2、 重新分片过程中,源节点和目标节点都可以继续处理命令请求(因为重新分片操作是键值对不断的迁移?)
操作流程:重新分片操作由Redis的集群管理软件 redis-trib 负责执行的
(对于单个槽的重新分片;多个槽就是单个槽的重复操作?)
1、通知目标节点准备导入属于槽slot的键值对。redis-trip向目标节点发送命令 (导入:import) 会修改目标节点的clusterState.importing_slots_from数组
> cluster setslot <slot> importing <source_id> //slot为槽的编号,source_id为源节点id
2、通知源节点准备迁移属于槽slot的键值对。redis-trip向源节点发送命令 (迁移:migrate) 会修改源节点的clusterState.migrating_slots_to数组
> cluster setslot <slot> migrating <target_id> //target_id为目标节点id
3、从源节点处获取属于槽slot的键值对。redis-trip向源节点发送命令
> cluster getkeysinslot <slot> <count> //count为最多获取count个键值对
4、将获得的键值对从源节点迁移到目标节点。对于获得的每一个键值对,redis-trip都向源节点发送一个migrate命令
> migrate <target_id> <target_port> <key_name> 0 <timeout> //目标节点id、端口、键名、超时时间设置
5、重复3、4两步操作,直到属于槽slot的所有键值对都成功从源节点迁移至目标节点
6、全部迁移成功后,将槽slot指派给目标节点。redis-trip向集群中任意一个节点发送命令,将slot指派给目标节点,
> cluster setslot <slot> NODE <target_id> //正式指派槽slot给界目标id的节点
然后这个信息会通过消息发送到整个集群,最终所有节点都会知道这个消息
(指派成功后,通过消息发送,通知整个集群中的节点,然后节点们根据消息,对节点内的clusterState结构和clusterNode结构进行更新?)
总结:先通知目标节点准备导入键值对,再通知源节点准备迁移键值对,然后开始键值对的迁移,键值对迁移成功后通过命令将槽指派给目标节点。键值对的迁移分为:从源节点获取键值对(1次最多获取count个),使用migrate命令将键值对从源节点迁移到目标节点,重复操作,直到键值对迁移完毕
需注意的是:如果经判断发现槽中没有保存键值对,则两步准备后直接将槽指派给目标节点即可。
6.命令执行流程(是否处理该槽,是否存在键,是否正在迁移,是否是ASK命令转向过来的等知识点)
流程图:
1. 根据算法计算给定键key属于哪个槽。值为i
CRC16(key) & 16383 //CRC校验和 + &16383计算出一个位于0~16383之间的整数
2. 判断该槽是否属于本节点的处理范围
通过clusterState.slots[i] == clusterState.myself 来判断
3.1 该槽在处理范围,则从当前节点的数据库中查找键
3.1.1 查找到键,则执行命令
3.1.2 找不到键,则判断该节点是否正在迁移该槽(经由clusterState.migrating_slots_to[16384]数组判断)
3.1.2.1 节点没有迁移该槽,则向客户端返回键查找不到错误。
3.1.2.2 节点正在迁移该槽,则向客户端返回ASK错误
> ASK i <ip>:<port> //i为键所在槽,ip和port分别为重新分片的目标节点的地址和端口
注意,ASK错误实际是不可见的
3.1.2.3 客户端根据ASK错误,转向找到迁移的目标节点
3.1.2.4 转向后,先向目标节点发送ASKING命令
3.1.2.5 发送ASKING命令后,再重新发送执行命令
3.2 该槽不在处理范围,判断客户端是否带有ASKING标识(即是否先发送了ASKING命令)
3.2.1 发送了ASKING标识,则破例执行关于该槽的命令一次
3.2.2 没有事先发送ASKING命令,则向客户端返回MOVED错误,形式如下
> MOVED i<键所在的槽> <ip>:<port> //后面为负责处理该槽的节点的 ip和port
注意,MOVED命令实际是不可见的。只有单机数据库下,客户端无法读懂该命令才会显示
3.2.2.2 客户端根据MOVED命令进行转向,转到指定节点,并执行命令
7.ASK错误和MOVED错误比较
相同:都会导致客户端转向,都是不可见的。客户端根据错误的信息自动转向
不同:1.MOVED错误代表将槽的负责权从一个节点转移到另一个节点,即永久转向;下次客户端遇到同样槽的命令会直接访问MOVED指向的节点
2.ASK错误只是两个节点在迁移槽的过程中使用的一种临时措施,下次客户端遇到同样槽的命令,仍然访问源节点(即目前负责节点)
直到槽迁移完成,再次访问就会收到MOVED命令
8.从节点,主节点,故障检测,故障转移(包括选取新主节点)
主节点负责处理槽,从节点用于复制某个主节点;当主节点下线时,在其所有从节点中选取一个用作主节点(类似备份?)(由Sentinel系统监控)
设置从节点
> cluster replicate <node_id> //接收命令的节点成为从节点,node_id为其主节点的id
设置从节点后,从节点对应的clusterNode的slaveof指针、flags属性做出相应修改;主节点的slaves指针数组、numsslaves属性也更新
集群中的节点之间通过互相发送消息的方式来交换集群中各个节点的状态信息
故障检测:节点间定期发送PING消息,以检测对方是否在线;在线,则应在规定时间内返回PONG消息;
超时未返回,则标记为疑似已下线(PFAIL)并更新对方节点对应的clusterNode的fail_reports链表(下线报告链表)
状态:在线、疑似已下线、已下线(FAIL)
当集群中一半以上主节点都将某个主节点X报告为疑似已下线时,则认为该主节点X已下线,并向集群广播关于其下线的FAIL消息,让其他节点将主节点X标记为已下线
故障转移:
1、从复制该主节点的所有从节点里选出一个从节点
2、该从节点执行 Slaveof no one 命令,成为新主节点
3、新主节点撤销所哟对已下线主节点的槽指派,将这些槽指派给自己
4、新主节点向集群广播一条PONG消息,通知其他节点自己已成为新主界定啊,让其他节点更新对应的clusterNode中的slots[]等属性
5、新主节点开始接受和处理相关命令请求
如何选取新主节点:
选举产生的。基于Raft算法。(Sentinel系统的领头Sentinel选举也采用这种方式)
涉及 (1)集群的配置纪元,是一个自增计数器,初始值为0;每进行一次故障转移操作,值+1
(2)每个主节点都有一次投票权,并且会把票投给第一个请求它投票的从节点(如果该主节点还未投票)
(3)从节点发现主节点下线后,会广播一条消息,向所有主节点请求获得投票支持
(4)主节点若向某个从节点投票,则投票同时会发送一条消息
(5)从节点对收到的消息进行统计
(6)设集群中有N个具有投票权的主节点,则当某个从节点收集到 N/2 + 1 张票时,该从节点成为新主节点
(7)若没有满足要求的从节点,则进入一个新的配置纪元,再次选举。
9.集群中的消息发送
集群中的节点通过发送消息和接收消息来进行通信。发送消息的节点为发送者sender,接收消息的节点为接收者receiver
常见的五种消息
-
MEET消息:发送者接收到客户端的 cluster meet 命令后,向指定节点发送MEET消息,邀请该节点进入发送者所在集群。
-
PING消息:用于检测节点是否在线。发送该消息有两种情况:(1)集群中每个节点默认每一秒胡会从已知节点中随机选取5个节点,并从5个节点中选取最长时间未发送过PING消息的节点发送PING消息。(2)如果节点A最后一次收到节点B的PONG消息时间距当前时间,已超过节点A的cluster-node-timeout设置时长的一半,则A向B发送PING消息,以防止更新滞后
-
PONG消息:接收者收到MEET消息或PING消息后,向发送者返回PONG消息,以通知发送者这条消息已到达。
-
FAIL消息:节点下线消息。当节点A判断节点B已下线时,则广播关于B的FAIL消息,让其他节点更改节点B的状态为已下线。
-
PUBLISH消息:当节点收到PUBLISH命令时,执行命令同时广播PUBLIS消息,所有节点都执行相同的PUBLISH命令。