哨兵解决和主从不能自动故障恢复的问题,但是同时也存在难以扩容以及单机存储、读写能力受限的问题,并且集群之前都是一台redis都是全量的数据,这样所有的redis都冗余一份,就会大大消耗内存空间。
redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特性。不需要**sentinel哨兵也能完成节点移除和故障转移的功能。需要将每个节点设置成集群模式,这种集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。redis集群的性能和高可用性**均优于之前版本的哨兵模式。集群模式实现了Redis数据的分布式存储,**实现数据的分片**,**每个redis节点存储不同的内容**,并且解决了在线的节点收缩和扩容问题。

本文对集群的节点,槽(slot),重新分片,转向,故障转移做介绍。
redis集群有节点组成,刚开始每个节点都是独立的,要组建一个可用的集群,需要把各个独立的节点连接起来。
加入节点:
CLUSTER MEET [ip1 port1]
redis-cli --cluster add-node [新节点ip:port] [旧主节点ip:port] --cluster-slave --cluster-master-id [id]
移除节点
重新设置主节点,只能针对slave节点操作
ps:指令实践参考:redis-基于docker搭建redis集群。
节点之间实现了将数据进行分片存储,那么节点之间又是怎么通信的呢?集群要作为一个整体工作,离不开节点之间的通信。在哨兵系统中,节点分为数据节点和哨兵节点:前者存储数据,后者实现额外的控制功能。在集群中,没有数据节点与非数据节点之分:所有的节点都存储数据,也都参与集群状态的维护。
集群中的每个节点,都提供了两个 TCP 端口:
普通端口+10000,如 7000 节点的集群端口为 17000。集群端口只用于节点之间的通信,如搭建集群、增减节点、故障转移等操作时节点间的通信;不要使用客户端连接集群接口。为了保证集群可以正常工作,在配置防火墙时,要同时开启普通端口和集群端口。维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:**集中 式和gossip**。redis cluster节点间采取gossip协议进行通信
集中式:
优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 如:zookeeper集中式存储元数据。
gossip:在节点数量有限的网络中,每个节点都“随机”的与部分节点通信,经过一番杂乱无章的通信,每个节点的状态很快会达到一致。优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据同步较慢。

节点间发送的消息主要分为 5 种:
meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等);ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了;PUBLISH命令后,会先执行该命令,然后向集群广播这一消息,接收节点也会执行该PUBLISH命令。所以加入一个新节点时:

老成员收到Meet消息后,在没有故障的情况下会回复PONG消息,表示欢迎新结点的加入,除了第一次发送Meet消息后,之后都会发送定期PING消息,实现节点之间的通信。
Redis 集群使用数据分片而非一致性哈希来实现: 一个Redis 集群包含 16384 个哈希槽(hash slot), 数据库中的每个键都属于这 16384 个哈希槽的其中一个, 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。
通过cluster nodes指令查看每个节点的槽信息。

Redis Cluster的哈希槽算法,CRC16(caicai)%16384 = 3967 这个key就被分配到了节点6371上 。如下图:


之所以进行分槽存储,是将一整堆的数据进行分片,防止单台的redis数据量过大,影响性能的问题。
注意:
当客户端向一个错误的节点发出了指令,该节点会发现指令的key所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连这个节点去获取数据。客户端收到指令后除了跳转到正确的节点上去操作,还会同步更新纠正本地的槽位映射表缓存,后续所有key将使用新的槽位映射表。


将任意数量已经指派给某个节点的槽,改为指派给另一个节点,槽对应的所有键值对都需要迁移。
特点:
操作流程:重新分片操作由Redis的集群管理软件 redis-trib 负责执行的 。
slot的键值对。redis-trip向目标节点发送命令 (导入:import) 会修改目标节点的clusterState.importing_slots_from数组。cluster setslot importing
//slot为槽的编号,source_id为源节点id
redis-trip向源节点发送命令 (迁移:migrate) 会修改源节点的clusterState.migrating_slots_to数组;cluster setslot migrating
//target_id为目标节点id
slot的键值对。redis-trip向源节点发送命令;cluster getkeysinslot //count为最多获取count个键值对
redis-trip都向源节点发送一个migrate命令;migrate
0 //目标节点id、端口、键名、超时时间设置
5、重复3、4两步操作,直到属于槽slot的所有键值对都成功从源节点迁移至目标节点;
6、全部迁移成功后,将槽slot指派给目标节点。redis-trip向集群中任意一个节点发送命令,将slot指派给目标节点。
cluster setslot NODE
//正式指派槽slot给界目标id的节点
命令执行流程(是否处理该槽,是否存在键,是否正在迁移,是否是ASK命令转向过来的等知识点)
总结:先通知目标节点准备导入键值对,再通知源节点准备迁移键值对,然后开始键值对的迁移,键值对迁移成功后通过命令将槽指派给目标节点。键值对的迁移分为:从源节点获取键值对,使用migrate命令将键值对从源节点迁移到目标节点,重复操作,直到键值对迁移完毕。
**需注意的是:如果经判断发现槽中没有保存键值对,则两步准备后直接将槽指派给目标节点即可。**
在进行重新切片的时候,源节点向目标节点迁移一个槽的过程中,可能出现这样的情况,属于被迁移的槽一部分键保存在源节点,另一部分在目标节点。

ASK 错误说明数据正在迁移,不知道何时迁移完成,因此重定向是临时的,SMART 客户端不会刷新 slots 缓存;MOVED 错误重定向则是(相对)永久的,SMART 客户端会刷新 slots 缓存。
主节点负责处理槽,从节点用于复制某个主节点;当主节点下线时,在其所有从节点中选取一个用作主节点(类似备份?)(由Sentinel系统监控)
设置从节点
cluster replicate <node_id> //接收命令的节点成为从节点,node_id为其主节点的id
设置从节点后,从节点对应的clusterNode的slaveof指针、flags属性做出相应修改;主节点的slaves指针数组、numsslaves属性也更新,集群中的节点之间通过互相发送消息的方式来交换集群中各个节点的状态信息。
通过定时任务发送 PING 消息检测其他节点状态;节点下线分为主观下线和客观下线;客观下线后选取从节点进行故障转移。与哨兵一样,集群只实现了主节点的故障转移;从节点故障时只会被下线,不会进行故障转移。
Slaveof no one命令,成为新主节点;PONG消息,通知其他节点自己已成为新节点,节点数量:在故障转移阶段,需要由主节点投票选出哪个从节点成为新的主节点;从节点选举胜出需要的票数为 N/2+1;其中 N 为主节点数量(包括故障主节点),但故障主节点实际上不能投票。因此为了能够在故障发生时顺利选出从节点,集群中至少需要 3 个主节点(且部署在不同的物理机上)。
故障转移时间:从主节点故障发生到完成转移,所需要的时间主要消耗在主观下线识别、主观下线传播、选举延迟等几个环节。
选举产生:基于Raft算法。(Sentinel系统的领头Sentinel选举也采用这种方式)当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:
slave发现自己的master变为FAIL;currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息;master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack;failover的slave收集master返回的FAILOVER_AUTH_ACK;slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的);slave广播Pong消息通知其他集群节点;从节点并不是在主节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票;
redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失。
规避方法可以在redis配置里加上参数(这种方法不可能百分百避免数据丢失)
min‐replicas‐to‐write 1 //写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如
集群总共三个节点可以配置1,加上leader就是2,超过了半数
注意:这个配置在一定程度上会影响集群的可用性,比如slave要是少于1个,这个集群就算leader正常也不能提供服务了,需要具体场景权衡选择。
对于类似mset,mget这样的多个key的原生批量操作命令,redis集群只支持所有key落在同一slot的情况,如果有多个key一定要用mset命令在redis集群上操作,则可以在key的前面加上{XX},这样参数数据分片hash计算的只会是大括号里的值,这样能确保不同的key能落到同一slot里去,示例如下:
mset {user1}:1:name caicai{user1}:1:age 18
这条命令在集群下执行,redis只会用大括号里的 user1 做hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot。
key 批量操作受限:例如 mget、mset 操作,只有当操作的 key 都位于一个槽时,才能进行keys/flushall 等操作:keys/flushall 等操作可以在任一节点执行,但是结果只针对当前节点。Lua 脚本,但前提条件是所涉及的 key 必须在同一个·节点。Redis 节点可以支持16个数据库,集群模式下只支持一个,即 db0。redis.conf的配置cluster-require-full-coverage为no时,表示当负责一个插槽的主库下线且没有相应的从yes则集群不可用。clusternodetimeout,表示当某个节点持续 timeout的时间失联时,才可以认定该节点出现故障,需要进行主从切换。如果没有这个选项,网络抖动会导致主从频繁切换 (数据的重新复制)。