集群是什么
主从结构解决的是可用性和并发量的问题,集群解决的是内存容量不足的问题,简单来讲,就是把所有数据分成几个部分分别存放在集群中的不同分片上。示意图如下:

数据分片算法
数据分片算法决定如何把数据映射到不同的分片上
哈希求余
数据所在的分片编号 = hash(数据的key)%分片的总个数(其中,hash是指把任意类型值转成数字的hash函数)
优点:简单高效,数据均匀分配
缺点:如果要扩容的话(增加分片),大部分数据要重新映射,搬运工作量巨大
一致性哈希算法
假设hash值的范围是****0 --- 2^32-1。可以把这个范围分成n段,也就是n个分片,数据的key的hash值属于哪个分片范围,就把它存到哪个分片里:

如果要扩容的话,就是将原有分片再分片:

这样一来,扩容的搬运成本就更低了,只需要将分片0中部分范围的数据搬运到分片3即可
优点:降低了搬运成本
缺点:数据不均匀分布(分片1,2范围大,分片0,3范围小)
哈希槽分区算法(实际使用)
一致性哈希算法就是因为新分片逮住一个就分片切割才造成了数据分布部均匀。哈希槽分区算法是在一致性哈希算法思想的基础上,把范围变成具体的一个个槽,以此来解决扩容后数据不均匀分布的问题
总共设置16384个槽,n个分片都平分这1638个槽。 某个数据被分配到的槽的编号 = hash(数据的key)%16384,这个槽属于哪个分片,数据就被存到哪个分片(每个分片使用16384个bit位表示自己包含哪些槽)****:

假设要扩容成4个分片,只需要每个分片都匀出一些槽给新分片即可,这样各个分片的数据仍旧是均匀的:

可以有16384个分片吗?
如果一个分片只对应一个槽,那么某分片闲置的概率就会加大,数据不均匀分布的概率就会加大。而且分片数目太多的话,对于整个集群的维护也是一个难题。所以redis作者建议分片个数不要超过1000。
为什么是16384个槽位?
节点之间通过心跳包进行通信,心跳包中需携带该节点所负责的槽位(slot)信息。这些槽位通常使用位图数据结构来表示。
- 一方面,以 Redis 集群为例,其槽位总数固定为 16384(16K),对应的位图大小为 2KB。若槽位数增加到 65536(64K),则位图体积将扩大至 8KB。虽然 8KB 对内存而言微不足道,但在高频的网络心跳包中,频繁传输这样的数据仍会带来不小的开销。
- 另一方面,Redis 集群官方建议分片数不超过 1000 个。因此,16384 个槽位对于最大 1000 个分片的场景已足够使用
集群的使用
集群构建:
cpp
redis-cli --cluster create 172.30.0.101:6379 172.30.0.102:6379 172.30.0.103:6379 172.30.0.104:6379 172.30.0.105:6379 172.30.0.106:6379 172.30.0.107:6379 172.30.0.108:6379 172.30.0.109:6379 --cluster-replicas 2
//cluster-replicas表明该集群中,每个分片有几个从节点
集群构建成功后,客户端可以连接集群中的任意一个节点,但是客户端对节点的所有操作都相当于对整个集群的操作,因为集群是一个整体。
需要注意的是:
- 如果客户端连接了某一分片,但是这个客户端存取的key没有映射在这个分片所维护的任何一个槽里,那么客户端就会收到命令失败的提示。那么客户端难道要每次先确认key在哪个分片才能发送命令吗?事实上,在登录客户端的时候加上-c选项就可以让客户端命令执行失败后,自动重定向到可以执行该命令的分片上。这依赖于每个分片都存着其他分片的信息。
- 理论上,集群无法很好支持同时操作多个key的命令,因为多个key不一定在同一个分片
集群的故障处理
集群具有类似于哨兵的功能,一但某个分片的主节点瘫痪,就会让该分片的从节点晋升为主节点,主节点恢复后自动变为新主节点的从节点。
故障判定
在Redis集群中,所有节点都会周期性地通过心跳包进行通信注意,是所有,整个集群中的每个节点不论主从,都会包含完整的信息,都会收发心跳包)。具体机制如下:
-
心跳交互:节点A向节点B发送
ping包,B收到后必须回复pong包。ping和pong仅在消息类型(message type)属性上不同,其余部分完全相同,都包含集群配置信息,例如节点ID、所属分片、主从角色、从属关系以及所持有的槽位(slots)位图等。 -
随机发送策略:每个节点每秒只会向部分随机节点发起
ping包,而不是向所有节点广播。这样设计是为了避免节点数量增多时心跳包数量爆炸式增长------若对全部节点发送,通信量将按节点数的平方级增长,严重影响网络开销。 -
主观下线(PFAIL):当节点A向B发送
ping后,若B未能如期响应,A会尝试重置与B的TCP连接。若重连依然失败,A便将B标记为PFAIL状态,即主观下线。 -
信息扩散(Gossip协议):A判定B为PFAIL后,会通过Redis内置的Gossip协议与其他节点沟通,向它们确认B的状态。每个节点都维护自己的"下线列表",由于视角不同,各节点的列表可能不一致。
-
客观下线(FAIL):若A发现集群中超过半数的节点也都认为B处于PFAIL状态,那么A就会将B正式标记为FAIL(客观下线),并将这一消息同步给集群中的其他节点,其他节点收到后也会将B标记为FAIL。至此,B被彻底认定为故障节点。
集群宕机条件
某些节点宕机可能引发整个集群进入fail状态,以下三种情况会导致集群宕机:
-
某个分片的所有主节点和从节点全部失效;
-
某个分片的主节点挂了,且该分片没有从节点可用;
-
超过半数的主节点同时宕机。
核心原则是:必须保证每个槽位(slot)都能正常工作,即每个分片至少要有一个可用的主节点,否则就算是整个集群都瘫痪了
故障迁移
当B被判定为FAIL后,集群会根据B的角色采取不同处理:
-
若B是从节点,则无需进行故障迁移;
-
若B是主节点,则其下属的从节点(例如C和D)将触发故障迁移流程,即从从节点中提拔一个新的主节点,继续为集群提供服务。
故障迁移的具体步骤如下:
-
参选资格判定:从节点首先检查自己是否有竞选资格。如果该从节点与主节点失联时间过长(超过阈值),说明其数据与主节点差异过大,则失去竞选资格。
-
休眠排序:具备资格的从节点(如C和D)会各自休眠一段时间,休眠时长计算公式为:
500ms基础时间 + [0, 500ms]随机时间 + 排名 × 1000ms,其中"排名"根据从节点复制主节点的偏移量(offset)决定,offset值越大,排名越靠前(即排名数值越小),因此数据越新的从节点休眠时间越短。 -
拉票竞选:休眠时间先到的节点(比如C)醒来后,会向集群中所有其他节点发起拉票请求,但只有主节点拥有投票权。
-
投票与晋升:每个主节点只有一票。当C获得的票数超过集群中主节点总数的一半时,C即当选为新的主节点。随后C执行
slaveof no one将自己变为主节点,并让其他从节点(如D)执行slaveof C,转而复制C。 -
信息同步:C将自身晋升为主节点的消息同步给集群中的所有节点,各节点收到后更新自己保存的集群拓扑结构。
上述选举过程基于Raft算法,这是一种在分布式系统中广泛使用的一致性算法。借助随机休眠时间的机制,通常谁先唤醒,谁就能率先发起拉票并竞选成功,从而高效完成故障恢复。
还可以看出,实际上集群中的每个节点不论主从,貌似有有相同的管理功能,比如重定向,检测故障等,这是集群的去中心化思想,这样的好处就是可用性很高并且扩展性很好(不需要重新部署配置什么的,因为大家都是同等地位)。
集群扩容
bash
redis-cli --cluster add-node 172.30.0.110:6379 172.30.0.101:6379
redis-cli --cluster add-node 172.30.0.111:6379 172.30.0.101:6379 --cluster-slave --cluster-master-id [集群中某节点的 nodeId]
//add-node 后的第⼀组地址是新节点的地址. 第⼆组地址是集群中的任意节点地址,表示集群本身
//--cluster-slave --cluster-master-id 指定把新节点作为哪个主节点的从节点
填加节点到集群后,节点自动成为一个master主节点,但这个分片默认没有任何的槽,所以需要执行下述命令进行槽的重新分配和数据的搬迁:
bash
redis-cli --cluster reshard 172.30.0.101:6379