(一)什么是Redis Cluster?
Redis Cluster是Redis的分布式解决方案,在Redis 3.0版本正式推出的,有效解决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构达到负载均衡的目的。
Redis Cluster 在5.0之后取消了ruby脚本 redis-trib.rb的支持(手动命令行添加集群的方式不变),集合到redis-cli里,避免了再安装ruby的相关环境。直接使用redis-clit的参数–cluster 来取代。关于Cluster的相关说明可以查看官网。
按照CAP理论来说,单机版的Redis属于保证CP(Consistency & Partition-Tolerancy)而牺牲A(Availability),也就说Redis能够保证所有用户看到相同的数据(一致性,因为Redis不自动冗余数据)和网络通信出问题时,暂时隔离开的子系统能继续运行(分区容忍性,因为Master之间没有直接关系,不需要通信),但是不保证某些结点故障时,所有请求都能被响应(可用性,某个Master结点挂了的话,那么它上面分片的数据就无法访问了)。
有了Cluster功能后,Redis从一个单纯的NoSQL内存数据库变成了分布式NoSQL数据库,CAP模型也从CP变成了AP。也就是说,通过自动分片和冗余数据,Redis具有了真正的分布式能力,某个结点挂了的话,因为数据在其他结点上有备份,所以其他结点顶上来就可以继续提供服务,保证了Availability。然而,也正因为这一点,Redis无法保证曾经的强一致性了。这也是CAP理论要求的,三者只能取其二。
Redis 集群中不存在中心节点或者代理节点, 集群的其中一个主要设计目标是达到线性可扩展性。
Redis 集群提供了一种运行 Redis 的方式,其中数据在多个 Redis 节点间自动分区。Redis 集群还在分区期间提供一定程度的可用性,即在实际情况下能够在某些节点发生故障或无法通信时继续运行。但是,如果发生较大故障(例如,大多数主站不可用时),集群会停止运行。
Redis使用中遇到的瓶颈
我们日常在对于redis的使用中,经常会遇到一些问题:
(1)高可用问题,如何保证redis的持续高可用性。
(2)容量问题,单实例redis内存无法无限扩充,达到32G后就进入了64位世界,性能下降。
(3)并发性能问题,redis号称单实例10万并发,但也是有尽头的。
Redis-Cluster的优势
(1)官方推荐,毋庸置疑。
(2)去中心化,集群最大可增加1000个节点,性能随节点增加而线性扩展。
(3)管理方便,后续可自行增加或摘除节点,移动分槽等等。
(4)简单,易上手。
(二)数据分布理论与Redis的数据分区
分布式数据库首要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整个数据的一个子集。常见的分区规则有哈希分区,范围分区,一致性哈希算法等。
Redis Cluster没有使用一致性hash, 而是引入了哈希槽的概念。
Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,每个key通过CRC16校验后对16383取模来决定放置哪个槽(Slot),每一个节点负责维护一部分槽以及槽所映射的键值数据。
计算公式:slot = CRC16(key) & 16383。

上图是一个key的分配存储流程:
客户端可以连接集群中的任意节点,将存储Key-Value中的Key,取CRC16运算结果,在取16383的模,得到的值一定在0-16383之间,再根据集群中每个节点托管的Solt配置,将请求路由到对应的主节点上。
如果是自己负责这个槽,那么直接执行命令,如果不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。
为什么RedisCluster会设计成16384个槽呢?
以下是通过Redis作者的回答的总结:
1.如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
如上所述,在消息头中,最占空间的是 myslots[CLUSTER_SLOTS/8]
。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
2.redis的集群主节点数量基本不可能超过1000个。
如上所述,集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者,不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
3.槽位越小,节点少的情况下,压缩率高
Redis主节点的配置信息中,它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中,会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
而16384÷8÷1024=2kb
这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。使用哈希槽的好处就在于可以方便的添加或移除节点。
- 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了。
- 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了。
(三)节点间的内部通信机制
1、基础通信原理
(1)redis cluster节点间采取gossip协议进行通信
跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的
集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到; 不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力
gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后
(2)10000端口
每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如7001,那么用于节点间通信的就是17001端口
每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong
(3)交换的信息
故障信息,节点的增加和移除,hash slot信息,等等
2、gossip协议
gossip协议包含多种消息,包括ping,pong,meet,fail,等等
meet: 某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信
ping: 每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据
每个节点每秒都会频繁发送ping给其他的集群,ping,频繁的互相之间交换数据,互相进行元数据的更新
pong: 返回ping和meet,包含自己的状态和其他信息,也可以用于信息广播和更新
fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了
3、ping消息深入
ping很频繁,而且要携带一些元数据,所以可能会加重网络负担
每个节点每秒会执行10次ping,每次会选择5个最久没有通信的其他节点
当然如果发现某个节点通信延时达到了cluster_node_timeout / 2,那么立即发送ping,避免数据交换延时过长,落后的时间太长了
比如说,两个节点之间都10分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题
所以cluster_node_timeout可以调节,如果调节比较大,那么会降低发送的频率
每次ping,一个是带上自己节点的信息,还有就是带上1/10其他节点的信息,发送出去,进行数据交换
至少包含3个其他节点的信息,最多包含总节点-2个其他节点的信息
(四)高可用性与主备切换原理
redis cluster的高可用的原理,几乎跟哨兵是类似的
1、判断节点宕机
如果一个节点认为另外一个节点宕机,那么就是pfail,主观宕机
如果多个节点都认为另外一个节点宕机了,那么就是fail,客观宕机,跟哨兵的原理几乎一样,sdown,odown
在cluster-node-timeout内,某个节点一直没有返回pong,那么就被认为pfail
如果一个节点认为某个节点pfail了,那么会在gossip ping消息中,ping给其他节点,如果超过半数的节点都认为pfail了,那么就会变成fail
2、从节点过滤
对宕机的master node,从其所有的slave node中,选择一个切换成master node
检查每个slave node与master node断开连接的时间,如果超过了cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成master
这个也是跟哨兵是一样的,从节点超时过滤的步骤
3、从节点选举
哨兵:对所有从节点进行排序,slave priority,offset,run id
每个从节点,都根据自己对master复制数据的offset,来设置一个选举时间,offset越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举
所有的master node开始slave选举投票,给要进行选举的slave进行投票,如果大部分master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成master
从节点执行主备切换,从节点切换为主节点

(五)Cluster实践
Redis版本6.2.1
3台服务器 192.168.65.100\101\102
每台启动2个服务,每个端口都不同:
100:redis-6379 \ redis-6380
101: redis-6381 \ redis-6382
102: redis-6383 \ redis-6384
主要修改的配置如下:
#端口,每个端口都不一样
port 6379
#指定一个pid_file
pidfile /var/run/redis_6379.pid
#在bind 绑定ip,我这边测试,所以是绑定0.0.0.0,具体按照实际情况来
bind 0.0.0.0
#找到Cluster配置的代码段,使得Redis支持集群
cluster-enabled yes
#每一个集群节点都有一个配置文件,这个文件是不能手动编辑的。确保每一个集群节点的配置文件不同
cluster-config-file nodes-6379.conf
#集群节点的超时时间,单位:ms,超时后集群会认为该节点失败默认是15000,为了测试我们把时间缩短
cluster-node-timeout 5000
appendonly yes
配置创建
配置好后,启动所有redis服务,使用任意节点创建集群,我们在100下创建
redis-cli –cluster create <ip:port> –cluster-replicas <number>
//创建集群命令:其中 cluster-replicas 1 代表 一个master后有几个slave,1代表为1个slave节点
[root@localhost qxconfig]# redis-cli --cluster create 192.168.65.100:6379 192.168.65.100:6380 192.168.65.101:6381 192.168.65.101:6382 192.168.65.102:6383 192.168.65.102:6384 --cluster-replicas 1
Performing hash slots allocation on 6 nodes…
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.65.101:6382 to 192.168.65.100:6379
Adding replica 192.168.65.102:6384 to 192.168.65.101:6381
Adding replica 192.168.65.100:6380 to 192.168.65.102:6383
M: 83c0133770f33c529652b8c7246fac29df3e2157 192.168.65.100:6379
slots:[0-5460] (5461 slots) master
S: 1e63b07c1dd41a142cc9f02ed72c73563c59eb01 192.168.65.100:6380
replicates 6572a385ec023317c80ddf10ec8360b47e433fc4
M: 6b9676d406b91168c652e1d787918cc4e05be423 192.168.65.101:6381
slots:[5461-10922] (5462 slots) master
S: 565f8c84759f20363ca3accfa03f001f73f9e2c1 192.168.65.101:6382
replicates 83c0133770f33c529652b8c7246fac29df3e2157
M: 6572a385ec023317c80ddf10ec8360b47e433fc4 192.168.65.102:6383
slots:[10923-16383] (5461 slots) master
S: 1338a22b5556c7a1f9080f346183b5bc9355c3d9 192.168.65.102:6384
replicates 6b9676d406b91168c652e1d787918cc4e05be423
Can I set the above configuration? (type 'yes' to accept): yes
Nodes configuration updated
Assign a different config epoch to each node
Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
.
Performing Cluster Check (using node 192.168.65.100:6379)
M: 83c0133770f33c529652b8c7246fac29df3e2157 192.168.65.100:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: 1338a22b5556c7a1f9080f346183b5bc9355c3d9 192.168.65.102:6384
slots: (0 slots) slave
replicates 6b9676d406b91168c652e1d787918cc4e05be423
S: 1e63b07c1dd41a142cc9f02ed72c73563c59eb01 192.168.65.100:6380
slots: (0 slots) slave
replicates 6572a385ec023317c80ddf10ec8360b47e433fc4
S: 565f8c84759f20363ca3accfa03f001f73f9e2c1 192.168.65.101:6382
slots: (0 slots) slave
replicates 83c0133770f33c529652b8c7246fac29df3e2157
M: 6572a385ec023317c80ddf10ec8360b47e433fc4 192.168.65.102:6383
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: 6b9676d406b91168c652e1d787918cc4e05be423 192.168.65.101:6381
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
[OK] All nodes agree about slots configuration.
Check for open slots…
Check slots coverage…
[OK] All 16384 slots covered. //创建成功
我们在不同服务器的客户端查看一下集群信息 Cluster info
[root@localhost qxconfig]# redis-cli -p 6379
//查看Cluster详情
127.0.0.1:6379> cluster info
cluster_state:ok //成功
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6 //节点
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:510
cluster_stats_messages_pong_sent:511
cluster_stats_messages_sent:1021
cluster_stats_messages_ping_received:506
cluster_stats_messages_pong_received:510
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:1021
==================================================
//查看集群节点配对信息
127.0.0.1:6384> cluster nodes
565f8c84759f20363ca3accfa03f001f73f9e2c1 192.168.65.101:6382@16382 slave 83c0133770f33c529652b8c7246fac29df3e2157 0 1615388394514 1 connected
6b9676d406b91168c652e1d787918cc4e05be423 192.168.65.101:6381@16381 master - 0 1615388394924 3 connected 5461-10922
83c0133770f33c529652b8c7246fac29df3e2157 192.168.65.100:6379@16379 master - 0 1615388394000 1 connected 0-5460
6572a385ec023317c80ddf10ec8360b47e433fc4 192.168.65.102:6383@16383 master - 0 1615388394000 5 connected 10923-16383
1338a22b5556c7a1f9080f346183b5bc9355c3d9 192.168.65.102:6384@16384 myself,slave 6b9676d406b91168c652e1d787918cc4e05be423 0 1615388393000 3 connected
1e63b07c1dd41a142cc9f02ed72c73563c59eb01 192.168.65.100:6380@16380 slave 6572a385ec023317c80ddf10ec8360b47e433fc4 0 1615388394000 5 connected
接下来我们查看,各节点slot的分配情况 使用命令 CLUSTER slots
127.0.0.1:6379> cluster slots
1) 1) (integer) 0
2) (integer) 5460
3) 1) "192.168.65.100"
2) (integer) 6379
3) "83c0133770f33c529652b8c7246fac29df3e2157"
4) 1) "192.168.65.101"
2) (integer) 6382
3) "565f8c84759f20363ca3accfa03f001f73f9e2c1"
2) 1) (integer) 10923
2) (integer) 16383
3) 1) "192.168.65.102"
2) (integer) 6383
3) "6572a385ec023317c80ddf10ec8360b47e433fc4"
4) 1) "192.168.65.100"
2) (integer) 6380
3) "1e63b07c1dd41a142cc9f02ed72c73563c59eb01"
3) 1) (integer) 5461
2) (integer) 10922
3) 1) "192.168.65.101"
2) (integer) 6381
3) "6b9676d406b91168c652e1d787918cc4e05be423"
4) 1) "192.168.65.102"
2) (integer) 6384
3) "1338a22b5556c7a1f9080f346183b5bc9355c3d9"
设置一些key 看看 redis-cli -c -h <ip> -p <port>
//[root@localhost qxconfig]# redis-cli -p 6382 //不使用-c
127.0.0.1:6379> set hello cluster //我们设置一个key
OK
127.0.0.1:6379> cluster keyslot hello //查看key在那一个slot
(integer) 866
127.0.0.1:6379> get hello //我们在所在slot的节点获取成功
"cluster"
127.0.0.1:6381> get hello //在其他slot的节点,返回了 MOVED , slot ,ip, port信息,指向了key所在的节点
(error) MOVED 866 192.168.65.100:6379
===================================================
[root@localhost qxconfig]# redis-cli -c -p 6382 //使用连接集群节点选项 -c
127.0.0.1:6382> get hello
-> Redirected to slot [866] located at 192.168.65.100:6379 //可以看到,进行了重定向获取
"cluster"
//变成了 100:6379
192.168.65.100:6379> set hello2 chris //用算法计算了slot后,自动重定向存储
-> Redirected to slot [7486] located at 192.168.65.101:6381
OK
//变成了101:6381
192.168.65.101:6381> get hello2
"chris"
================================================
//使用事务 ,如果出现涉及其他节点的命令,会直接重定向导致事务失败
192.168.65.101:6381> multi
OK
192.168.65.101:6381(TX)> set name chris
QUEUED
192.168.65.101:6381(TX)> get hello
-> Redirected to slot [866] located at 192.168.65.100:6379
"cluster"
192.168.65.100:6379> get hello2
-> Redirected to slot [7486] located at 192.168.65.101:6381
"chris"
192.168.65.101:6381> get name
(nil)
=================================================
192.168.65.101:6381> multi
OK
192.168.65.101:6381(TX)> set name chris
QUEUED
192.168.65.101:6381(TX)> set hello3 redis
-> Redirected to slot [3359] located at 192.168.65.100:6379
OK
192.168.65.100:6379> get hello3
"redis"
192.168.65.100:6379> get name
-> Redirected to slot [5798] located at 192.168.65.101:6381
(nil)
故障转移
//我们kill掉一个 Master 100:6379
[root@localhost qxconfig]# ps -ef |grep redis
root 8316 1 0 22:40 ? 00:00:05 redis-server 0.0.0.0:6379 [cluster]
root 8395 1 0 22:40 ? 00:00:05 redis-server 0.0.0.0:6380 [cluster]
root 12634 1722 0 23:49 pts/0 00:00:00 grep --color=auto redis
[root@localhost qxconfig]# kill -9 8316
[root@localhost qxconfig]# redis-cli -c -p 6380
127.0.0.1:6380> cluster nodes
.....省略 192.168.65.102:6383@16383 master - 0 1615391395000 5 connected 10923-16383
192.168.65.102:6384@16384 slave 6b9676d406b91168c652e1d787918cc4e05be423 0 1615391394000 3 connected
192.168.65.100:6380@16380 myself,slave 6572a385ec023317c80ddf10ec8360b47e433fc4 0 1615391393000 5 connected
192.168.65.101:6382@16382 master - 0 1615391395554 7 connected 0-5460
192.168.65.100:6379@16379 master,fail - 1615391381540 1615391379000 1 disconnected
192.168.65.101:6381@16381 master - 0 1615391395757 3 connected 5461-10922
//可以看到fail后,选举了一个slave作为Master ,就是继承了0-5460 solt 的 101:6382@16382
127.0.0.1:6380> get hello
-> Redirected to slot [866] located at 192.168.65.101:6382 //hello key依旧存在
"cluster"
======================================================
重连
[root@localhost qxconfig]# redis-server redis.conf
[root@localhost qxconfig]# redis-cli -c -p 6379
127.0.0.1:6379> cluster nodes
192.168.65.101:6381@16381 master - 0 1615391712000 3 connected 5461-10922
192.168.65.102:6383@16383 master - 0 1615391712761 5 connected 10923-16383
192.168.65.102:6384@16384 slave 6b9676d406b91168c652e1d787918cc4e05be423 0 1615391712000 3 connected
192.168.65.100:6380@16380 slave 6572a385ec023317c80ddf10ec8360b47e433fc4 0 1615391712559 5 connected
192.168.65.101:6382@16382 master - 0 1615391712051 7 connected 0-5460
192.168.65.100:6379@16379 myself,slave 565f8c84759f20363ca3accfa03f001f73f9e2c1 0 1615391711000 7 connected
//重连后变成了slave
集群客户端命令(redis-cli -c -p port)
集群
cluster info :打印集群的信息
cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。
节点
cluster meet <ip> <port> :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。
cluster forget <node_id> :从集群中移除 node_id 指定的节点。
cluster replicate <node_id> :将当前节点设置为 node_id 指定的节点的从节点。
cluster saveconfig :将节点的配置文件保存到硬盘里面。
槽(slot)
cluster addslots <slot> [slot ...] :将一个或多个槽( slot)指派( assign)给当前节点。
cluster delslots <slot> [slot ...] :移除一个或多个槽对当前节点的指派。
cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。
cluster setslot <slot> node <node_id> :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给
另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。
cluster setslot <slot> migrating <node_id> :将本节点的槽 slot 迁移到 node_id 指定的节点中。
cluster setslot <slot> importing <node_id> :从 node_id 指定的节点中导入槽 slot 到本节点。
cluster setslot <slot> stable :取消对槽 slot 的导入( import)或者迁移( migrate)。
键
cluster keyslot <key> :计算键 key 应该被放置在哪个槽上。
cluster countkeysinslot <slot> :返回槽 slot 目前包含的键值对数量。
cluster getkeysinslot <slot> <count> :返回 count 个 slot 槽中的键