Redis官方在3.0版本推出了自己的分布式方案,这就是Redis Cluster。
Redis Cluster 提供了一种去中心化
的分布式方案,该方案可以实现水平拆分,故障转移等需求。
其拓扑结构
通过一个简单的额例子说明:一个Redis Cluster由多个Redis节点组成,这里的节点指的是Redis实例,一台服务器上可能有多个实例。
假设 有 1、2、3、4、5、6、号redis实例,其中 1和2 是一个节点组
, 且 1号是 master 2号是 slave;
同理 3 和 4 是一个节点组
, 且 3号是 master 4号是 slave; 一次类推 一共有 3个 节点组。
也就是全量数据被划分为3份,分别标号1/2/3。
是Redis分布式的基础。Redis中包含两种节点:master节点与slave节点。
同一份数据存放在master与多个slave节点上,master对外提供读写,slave不对外提供写操作。
当master宕机时,slave节点还能继续提供服务。主从复制中最重要的就是 master 和 slave 之间数据如何同步。
主从同步分为 全量同步 和 增量同步
主节点上进将当前内存的数据全部快照写到磁盘文件中,然后将文件同步给从节点。
从节点清空当前内存全部数据后全量加载该文件,这样达到与主节点数据同步的目的。
但是在从节点加载快照文件的过程中,主节点还在 对外提供写服务。
所以当从节点加载完快照后,依旧可能与主节点数据不一致,这时就需要增量同步上场了。
增量同步的不是数据,而是
指令流
, 这个 指令流 类似于 mysql 主从复制 的 binlog。
主节点会将对当前数据状态产生修改的指令记录在内存的一个buffer
中,
然后异步地将buffer同步到从节点,从节点通过执行buffer中的指令,达到与主节点数据一致的目的。
Redis的buffer是一个
定长的环形结构
,当指令流满的时候, 一定会覆盖最前面的内容。
所以当从节点上次增量同步由于各种原因,导致花费时间较长时,
再次同步指令流时,就有可能前面没有同步的指令被覆盖掉了。
这种情况就需要进行全量同步了。
当master宕机时,需要 人工将slave节点切换成master,这 显然是无法接受的。
所以需要有一个机制,当master宕机时能自动进行主从切换,应用程序无感知,继续提供服务。
于是Redis官方提供了一种方案:Redis Sentinel
(哨兵模式)。
简单的说,哨兵模式就是在主从基础上增加了哨兵节点,哨兵节点不存储业务数据,它负责监控主从节点的健康,当主节点宕机时,它能及时发现,并自动选择一个从节点,将其切换成主节点。为了避免哨兵本身成为单点,哨兵一般也由多个节点组成
所谓分片(Sharding),就是将 数据集 按照一定规则,分散存储在 各个节点上。这里涉及两个问题:
- 分片规则是什么?
- 如何存储在各个节点上?
Redis将所有数据分为
16384个hash slot(槽)
,每条数据(key-value)根据key值通过算法映射到其中一个slot上,
这条数据就存储在该slot中。映射算法是:slotId=crc16(key)%16384
Redis的每个key都会基于该分片规则,落到特定的slot上。而在集群部署完成时,slot的分布就已经确定了。对于一个稳定的集群,slot的分布也是固定的。但在一些情况下,slot的分布需要发生改变:
- 新的master加入
- 节点分组退出集群
- slot分布不均匀
这些情况下就需要进行slot的迁移。slot迁移的触发与过程控制都是由外部系统完成,Redis只提供能力,但不自动进行slot迁移。
当客户端向某个节点发出指令,该节点发现指令的key对应的slot不在当前节点上,
这时Redis会向客户端发送一个MOVED指令,告诉它正确的节点,
然后客户端去连这个正确的节点并进行再次操作。如下,15495为key a所在的slot id。172.16.190.78:7001> get a (error) MOVED 15495 172.16.190.77:7000
- 1
- 2
在slot迁移过程中 的一种错误返回。当某个slot在迁移过程中,
客户端发了一个位于该slot的某个key的操作请求,请求被路由到旧的节点。
此时该key如果在旧节点上存在,则正常操作;如果在旧的节点上找不到,
那么可能该key已被迁移到新的节点上,也可能就没有该key,
此时会返回ASKING,让客户端跳转到新的节点上去执行。
MOVED 是永久重定向,下次对同样的key 进行操作,客户端就将请求发送到正确的节点,
ASKING 是临时重定向,它只对这次操作起作用,不会更新客户端的槽位关系表。
Redis Cluster没有专门用于维护节点状态的节点,而是所有节点通过 Gossip协议相互通信,
广播自己的状态以及自己对整个集群认知的改变。
如果一个节点宕掉了,其他节点和它进行通信时,会发现改节点失联。当某个节点发现其他节点失联时,
会将这个失联节点状态变成PFail(possible fail)
,并广播给其他节点。
当一个节点收到某个节点PFail的数量达到了主节点的大多数
,就标记该节点为Fail,并进行广播,通过这种方式确认节点故障。
当slave发现其master状态为Fail后,它会发起选举,如果其他master节点都同意,则该slave进行从主切换,变成master节点。
同时会将自己的状态广播给其他节点,达到大家信息一致性。
这里采用docker的形式 部署 redis cluster。
# 获取镜像
docker pull redis:5.0.9-alpine3.11
# 自定义一个网络
docker network create redis --subnet 172.28.0.0/16
通过 脚本文件 generate_redis_config.sh 创建6个 redis 配置
for port in $(seq 1 6); \
do \
mkdir -p ./redis/node-${port}/conf
touch ./redis/node-${port}/conf/redis.conf
cat << EOF > ./redis/node-${port}/conf/redis.conf
port 6379
bind 0.0.0.0
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.28.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
appendonly yes
EOF
done
脚本文件输出:
redis/
├── node-1
│ └── conf
│ └── redis.conf
├── node-2
│ └── conf
│ └── redis.conf
├── node-3
│ └── conf
│ └── redis.conf
├── node-4
│ └── conf
│ └── redis.conf
├── node-5
│ └── conf
│ └── redis.conf
└── node-6
└── conf
└── redis.conf
12 directories, 6 files
通过脚本文件 generate_redis_container.sh 生成6个redis容器
# 容器1
docker run -p 6371:6379 -p 16371:16379 --name redis-1 \
-v ${PWD}/redis/node-1/data:/data \
--mount type=bind,src=${PWD}/redis/node-1/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.11 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器2
docker run -p 6372:6379 -p 16372:16379 --name redis-2 \
-v ${PWD}/redis/node-2/data:/data \
--mount type=bind,src=${PWD}/redis/node-2/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.12 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器3
docker run -p 6373:6379 -p 16373:16379 --name redis-3 \
-v ${PWD}/redis/node-3/data:/data \
--mount type=bind,src=${PWD}/redis/node-3/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.13 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器4
docker run -p 6374:6379 -p 16374:16379 --name redis-4 \
-v ${PWD}/redis/node-4/data:/data \
--mount type=bind,src=${PWD}/redis/node-4/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.14 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器5
docker run -p 6375:6379 -p 16375:16379 --name redis-5 \
-v ${PWD}/redis/node-5/data:/data \
--mount type=bind,src=${PWD}/redis/node-5/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.15 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
# 容器6
docker run -p 6376:6379 -p 16376:16379 --name redis-6 \
-v ${PWD}/redis/node-6/data:/data \
--mount type=bind,src=${PWD}/redis/node-6/conf/redis.conf,dst=/etc/redis/redis.conf \
-d --net redis --ip 172.28.0.16 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
docker exec -it redis-1 /bin/sh
redis-cli --cluster create 172.28.0.11:6379 172.28.0.12:6379 172.28.0.13:6379 172.28.0.14:6379 172.28.0.15:6379 172.28.0.16:6379 --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 172.28.0.15:6379 to 172.28.0.11:6379
Adding replica 172.28.0.16:6379 to 172.28.0.12:6379
Adding replica 172.28.0.14:6379 to 172.28.0.13:6379
M: cfb09d104f563d5dea870437cf73fac266b69a30 172.28.0.11:6379
slots:[0-5460] (5461 slots) master
M: 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c 172.28.0.12:6379
slots:[5461-10922] (5462 slots) master
M: be4034133574260765165beac5fd1edacb63a2bd 172.28.0.13:6379
slots:[10923-16383] (5461 slots) master
S: 74685c90c5aebdc8a035d288c962e22f98e994e8 172.28.0.14:6379
replicates be4034133574260765165beac5fd1edacb63a2bd
S: 96b4185cc40b532b144b2ac3e2fe7213e7c13a50 172.28.0.15:6379
replicates cfb09d104f563d5dea870437cf73fac266b69a30
S: edd2e12510d921be2193df5b461033fc0f465144 172.28.0.16:6379
replicates 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c
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 172.28.0.11:6379)
M: cfb09d104f563d5dea870437cf73fac266b69a30 172.28.0.11:6379
slots:[0-5460] (5461 slots) master
1 additional replica(s)
S: 96b4185cc40b532b144b2ac3e2fe7213e7c13a50 172.28.0.15:6379
slots: (0 slots) slave
replicates cfb09d104f563d5dea870437cf73fac266b69a30
S: edd2e12510d921be2193df5b461033fc0f465144 172.28.0.16:6379
slots: (0 slots) slave
replicates 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c
M: be4034133574260765165beac5fd1edacb63a2bd 172.28.0.13:6379
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
M: 47d28c2cc92ce7fafbe09b14d521a35e56a2c02c 172.28.0.12:6379
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
S: 74685c90c5aebdc8a035d288c962e22f98e994e8 172.28.0.14:6379
slots: (0 slots) slave
replicates be4034133574260765165beac5fd1edacb63a2bd
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
-c表示集群
)redis-cli -c
cluster info
集群创建好之后,11,12,13为主节点,其余为从节点
cluster nodes
# 输出
ec2d5b58fb0572e06ba8eeac62f87c595fa73913 172.28.0.14:6379@16379 slave ad075641de017884f05e9b17365a956994912f77 0 1656504631515 4 connected
3d7137bd841fd90e37880e3fa2dc0eeb6586ea18 172.28.0.12:6379@16379 slave 8dcfe2052c7a278d2de90a1ee9b48d287f11c2a7 0 1656504632018 7 connected
8dcfe2052c7a278d2de90a1ee9b48d287f11c2a7 172.28.0.16:6379@16379 master - 0 1656504630000 7 connected 5461-10922
ad075641de017884f05e9b17365a956994912f77 172.28.0.13:6379@16379 master - 0 1656504630000 3 connected 10923-16383
0e0dcd720fee1821826fb9808ad0d3446afc3fe4 172.28.0.15:6379@16379 slave 172c41d7253e68cb3564f115b21450575f43e5d3 0 1656504630513 5 connected
172c41d7253e68cb3564f115b21450575f43e5d3 172.28.0.11:6379@16379 myself,master - 0 1656504631000 1 connected 0-5460
set name Stephen
-> Redirected to slot [5798] located at 172.28.0.12:6379
OK
docker stop redis-2
get name
-> Redirected to slot [5798] located at 172.28.0.16:6379
"stephen"
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 将节点的配置文件保存到硬盘里面
cluster slaves <node_id> 列出该slave节点的master节点
cluster set-config-epoch 强制设置configEpoch
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槽中的键
cluster myid 返回节点的ID
cluster slots 返回节点负责的slot
cluster reset 重置集群,慎用