Redis Cluster集群

哨兵模式架构的缺点

哨兵模式架构

  1. 使用哨兵模式的话,如果master节点挂了,哨兵模式会从slave中选举出新的master,但是在选举的过程中 需要几秒到数十秒的时间,这个时间内 客户端是无法正常访问redis服务的,这种情况也称为 访问瞬断。
  2. 哨兵模式下只有一个主节点对外提供服务, 没办法支持 高并发场景,且单节点的内存也不可以设置太大(推荐小于10G),否则持久化和数据同步都会很慢。

高可用集群模式

Redis Cluster

  • redis集群是 由多个小的主从节点组成的分布式服务器群,客户端通过cluster访问整个redis集群。

  • 在集群里面的单个主从节点架构都是小集群,在小集群里面如果主节点挂了, 不需要哨兵也能在从节点里面选举出新的主节点

  • 客户端第一次连接到集群时,会获取到整个大集群的所有master节点信息和slot槽位信息并保存到客户端本地。

  • 客户端存储数据时, 使用cluster工具会根据key进行CRC16算法后取模(%16384),计算出具体的slot位置,找到slot具体的master节点,连接该小集群的master节点存储数据。这就意味着 每个小集群数据都不会重叠。

    • 如:client发送命令:set cluster test
      • client使用cluster工具会根据key进行CRC16算法后取模,计算出slot,发送给对应的master节点
  • 在整个redis集群内,小集群架构可以横向扩展到上万个集群,但是官方 建议不要超过1000个集群,否则集群性能会大幅下降。

集群架构的优点

  1. 单个小集群可以存储10个G,如果横向扩展成100个集群,则可以存储1T的数据,以此类推,最高可以达到1000个集群
    • redis单节点的内存建议小于10G,如果内存太大的话,在redis进行持久化和数据同步(主从)时,会给主节点造成很大的压力。
  2. 单个小集群的主节点可以支持10W的QPS并发访问,如果横向扩展成100个集群,则支持1000W的QPS并发访问。

在大型的互联网公司里面,如 新浪微博、腾讯等,因为数据量巨大,他们会存在多个redis集群,比如 用户模块 单独一个redis集群,文章模块单独一个redis集群,以此类推。

问题:集群架构是否解决了 哨兵架构 访问瞬断的问题?

image-1

没有完全解决,比如客户端发送了get命令,客户端的cluster工具 通过hash分片算法计算出slot后,发送给对应的master节点,如果此时 主节点挂了,slave还在选举的过程中,是无法对外提供服务的。

image-2

但是其他的小集群节点是正常工作的,如果客户端根据key通过hash分片算法 定位到其他的小集群节点,是可以正常访问的。

集群架构的缺点

  • 集群架构下的小集群 只允许master读写,slave是无法读写的,只是作为一个冷备(冗余备份)服务器,当master挂了后去代替master工作。

Redis Cluster集群搭建

搭建视频:https://xing-video.oss-cn-hangzhou.aliyuncs.com/redis%E9%AB%98%E5%8F%AF%E9%AB%98%E9%9B%86%E7%BE%A4%E6%9E%B6%E6%9E%84%E6%90%AD%E5%BB%BA.mp4

要搭建redis集群,至少需要3个master节点(为什么至少需要3个节点,请往下翻《Redis集群master节点限制》)。我们这里搭建3个master节点,并且给每个master节点配上一台slave节点(一主一从),共计6个redis节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 第一步:把之前的redis.conf配置文件copy到cluster目录并修改以下内容(随便叫什么名字都可以,只要以该配置文件启动就行)

// 后台运行
daemonize yes


// 注意:如果是一台机器运行一个redis,端口号,pidFile、dir不需要调整,如果是一台机器运行一主一从或多个redis节点,端口号、pidFile和dir保存路径都得改成不同的
// 端口号
port 6379
pidfile /var/run/redis_6379.pid
dir /usr/local/redis/bin/cluster/6379/

// 启动集群模式
cluster-enabled yes

// 设置集群节点文
cluster-config-file nodes-6379.conf

// 设置集群节点超时时间
cluster-node-timeout 15000

// (bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通 过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
#bind 127.0.0.1 -::1

// 关闭保护模式
protected-mode no

// 开启AOF持久化
appendonly yes

// 如果要设置密码需要增加如下配置:
// 注意,在redis 6.0后的版本 引入了acl策略(AccessControlList),如果在配置中设置密码很可能不会生效(实测),具体得结合acl策略
requirepass 123123 // 设置redis密码
masterauth 123123 // (设置集群节点间访问密码,跟上面一致)

// 执行这条命令需要确认三台机器之间的redis实例要能相互访问,可以先简单把所有机器防火墙关掉,如果不关闭防火墙则需要打开redis服务端口和集群节点gossip通信端口16379(默认是在redis端口号上加1W)
# 关闭防火墙
systemctl stop firewalld # 临时关闭防火墙
systemctl disable firewalld # 禁止开机启动


// 启动集群,--cluster-replicas 1 ,后面的1 代表一个小集群架构有几个从机
./redis-cli --cluster create --cluster-replicas 1 10.211.55.6:6379 10.211.55.7:6379 10.211.55.8:6379 10.211.55.9:6379 10.211.55.10:6379 10.211.55.11:6379
// 在一台机器上运行就可以了,他会去维护其他机器 进入集群模式


// 如果需要关闭集群,需要逐个redis节点关闭
./redis-cli ‐c ‐h 110.211.55.6:6379 ‐p 6379 shutdown

slot hash槽分配

  • 创建集群时会把16384个slot分片 尽量平均分配到集群中所有master节点。

如当前集群有3台master节点,就用16384 / 3 ~=5461,尽量平均分配各每个master节点。

注意:slot分片只是一个逻辑的概念,目的是为了让客户端的cluster可以通过key进行CRC16算法后再进行取模,快速定位到具体的主节点小集群。

如:client发送 set a a的命令,客户端的cluster把key a 通过 CRC16算法后再进行取模, 计算出属于2192槽位,该槽位归属于 第一个小集群,则连接第一个小集群的master主机发送命令。

image-3

启动成功后,我们可以通过以下2条命令,在客户端里面查看集群信息:

1
2
// 先通过客户端 连接集群,-c 代表 集群模式,-h 集群的节点IP和端口号
[root@S1 bin]# ./redis-cli -c -h 10.211.55.6 -p 6379

cluster info(查看集群信息)

cluster info信息截图

cluster nodes(查看节点列表)

cluster-nodes

如图所示,10.211.55.10的从机对应的主机是10.211.55.6,以此类推。从机后面绑定的是主机的节点唯一ID。

Redis Cluster集群水平扩展

在上面的章节,我们已经描述过如何搭建一个cluster集群,下面我们将对该集群进行 增加(扩容)和删除redis集群节点。

Redis Cluster集群水平扩展

1、启动集群

1
2
3
4
5
6
7
8
// 如果关闭了集群后,再次启动集群 不需要再创建集群,而是直接以cluster配置文件启动redis节点就行
// 如果您之前的集群并没有关闭,则不需要再次启动集群
[root@s1 bin]# ./redis-server cluster/redis.conf
[root@s2 bin]# ./redis-server cluster/redis.conf
[root@s3 bin]# ./redis-server cluster/redis.conf
[root@s4 bin]# ./redis-server cluster/redis.conf
[root@s5 bin]# ./redis-server cluster/redis.conf
[root@s6 bin]# ./redis-server cluster/redis.conf

2、查看集群连接状态

cluster nodes

增加小集群实例

接下来我们在原本的集群上,在增加 一个小集群(一主一从,红色框框)。

redis cluster扩展示意图

启动redis节点

  • 2个节点,一主一从都按以下配置 启动redis节点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 从redis源码中复制配置文件到cluster文件
[root@S7 bin]# cp /root/redis-6.2.5/redis.conf /usr/local/redis/bin/cluster/redis.conf

// 编辑cluster配置文件
[root@S7 bin]# vim cluster/redis.conf

// 后台运行
daemonize yes


// 注意:如果是一台机器运行一个redis,端口号,pidFile、dir不需要调整,如果是一台机器运行一主一从或多个redis节点,端口号、pidFile和dir保存路径都得改成不同的
// 端口号
port 6379
pidfile /var/run/redis_6379.pid
dir /usr/local/redis/bin/cluster/6379/

// 启动集群模式
cluster-enabled yes

// 设置集群节点文
cluster-config-file nodes-6379.conf

// 设置集群节点超时时间
cluster-node-timeout 15000

// (bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通 过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)
#bind 127.0.0.1 -::1

// 关闭保护模式
protected-mode no

// 开启AOF持久化
appendonly yes

// 如果要设置密码需要增加如下配置:
// 注意,在redis 6.0后的版本 引入了acl策略(AccessControlList),如果在配置中设置密码很可能不会生效(实测),具体得结合acl策略
requirepass 123123 // 设置redis密码
masterauth 123123 // (设置集群节点间访问密码,跟上面一致)

// 执行这条命令需要确认三台机器之间的redis实例要能相互访问,可以先简单把所有机器防火墙关掉,如果不关闭防火墙则需要打开redis服务端口和集群节点gossip通信端口16379(默认是在redis端口号上加1W)
# 关闭防火墙
systemctl stop firewalld # 临时关闭防火墙
systemctl disable firewalld # 禁止开机启动

// 启动redis节点
./redis-server cluster/redis.conf

查看redis cluster命令帮助

[root@S8 bin]# ./redis-cli --cluster help

cluster help

create:创建一个集群环境

call:执行redis命令

add-node:将一个节点添加到集群里,第一个参数为新节点的ip:port,第二个参数为集群中任意一个已经存在的节点的ip:port

del-node:移除一个节点

reshard:重新分片

check:检查集群状态

配置新的master节点

接下来我们通过命令add-node把10.211.55.12配置成集群中的master节点。

1
2
//             集群   新增节点   新增的master节点的   集群中已知的任意节点
./redis-cli --cluster add-node 10.211.55.12:6379 10.211.55.10:6379

添加成功后会提示 [OK] New node added correctly.

cluster新增节点

查看集群节点状态

cluster nodes

当节点添加成功后,新增的节点不会有任何数据,也不能对外提供服务,因为节点还 没有分配slot槽(HASH槽),我们需要手动为他分配slot槽。

手动分配slot槽

注意:从其他节点迁移slot到新的节点,同时会把slot对应的数据也迁移过去。

1
2
                    // 分片命令   redis集群中已知任意一个节点和端口号,可以不是要迁移的master节点,这里只是为了进入客户端
./redis-cli --cluster reshard 10.211.55.10:6379

输出如下:

1、How many slots do you want to move (from 1 to 16384)?

解释:需要分配多少个slot到新的节点上?这个自行设置,如1000个

slot哈希槽分配

2、What is the receiving node ID?

解释:需要把这1000个slot分配到哪个节点,这里的节点标识是最左边的节点唯一标识,通过ip地址可以找到

3、Please enter all the source node IDs.

image-1

解释:请输入需要调整的源节点(从哪个倒霉蛋上分割出slot给新的master)

Type ‘all’ to use all the nodes as source nodes for the hash slots.

输入all,则平均的从所有master节点上分割出slot给新的master节点,我们这里选择 all

Type ‘done’ once you entered all the source nodes IDs.

如果要制定哪个倒霉蛋的话,就要先输入 节点唯一标识,然后在输入done,这点和删除之前 要先调整slot一样,不明白的可以看下面的章节。

4、Do you want to proceed with the proposed reshard plan (yes/no)?

解释:输入yes 同意上面的分配计划,并执行分配任务

同意分配哈希槽

查看分配slot后的节点信息

查看分配slot后的节点信息

如图所示,迁移成功了,我们迁移计划是迁移1000个slot,redis会从所有master节点 平均的 迁移出部分slot,加起来刚好1000个slot。

值得注意的是:迁移slot的同时会把slot对应的数据也迁移到master节点。

创建过程动图:

添加master节点过程动图

配置新的slave节点

接下来我们将10.211.55.13配置成新的slave节点,但是得先加入redis集群中。

1
2
//                            集群   新增节点   新增的master节点的   集群中已知的任意节点
[root@S8 bin]# ./redis-cli --cluster add-node 10.211.55.13:6379 10.211.55.10:6379
配置新的slave节点

值得注意的是,任何一个节点刚加入集群的时候状态都是master,我们后面给他手动调整成 slave。

cluster nodes

为slave指定master节点

  • 我们需要为哪台slave指定master节点,我们就登录该slave的client 客户端,并使用集群命令进行操作
1
2
3
4
[root@S8 bin]# ./redis-cli -c -h 10.211.55.13
//指定slave 复制数据的master节点(集群唯一标识,如果不知道的话可以在客户端中通过 cluster nodes命令获取)
10.211.55.13:6379> cluster replicate 23c291df3e36b695c97b3304e1e357815da01400
OK

查看cluster nodes 集群节点信息

cluster nodes

此时10.211.55.13 状态已经变成了slave,并且 已经是10.211.55.12(master)的从节点。

删除小集群实例

删除slave相对简单些,直接可以删除,删除master步骤比较麻烦,需要先把slot槽迁移给其他节点后才可以删除。

如果想要删除整个小集群,建议先删除所有的slave节点后再删除master节点。

删除slave节点

1
2
3
4
5
// 												集群模式	删除节点		删除节点的IP:PORT	节点唯一标识(在redis客户端可通过cluster nodes获取)
[root@s1 bin]# ./redis-cli --cluster del-node 10.211.55.13:6379 8f4eb71b46bdccf2b13faa4d048740a5d10571da
>>> Removing node 8f4eb71b46bdccf2b13faa4d048740a5d10571da from cluster 10.211.55.13:6379
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

删除成功,我们通过cluster nodes命令看一下节点信息,发现已经没有10.211.55.13 slave节点了。

cluster nodes

删除master节点

  • 删除master节点之前,必须先将master节点的slot槽位迁移给其他可用的master节点(只能指定一个节点,无法全部节点平均分配),然后才进行删除操作。

    • 如果不迁移直接删除的话,会造成数据丢失!

1、迁移slot槽位

1
2
                    // 分片命令   redis集群中已知任意一个节点和端口号,可以不是要删除的master节点,这里只是为了进入客户端
./redis-cli --cluster reshard 10.211.55.10:6379

1、How many slots do you want to move (from 1 to 16384)? 1000

解释:需要分配多少个slot到新的节点上?删除之前要先迁移,slot就是即将删除的master节点的所有slot槽位数量

调整slot

1.2、What is the receiving node ID? 282af68b67e015c53d8f4abdaf3ac5ea14a9a2f2

解释:需要把slot槽位迁移到哪个节点?这里我们指定 集群中可用的任意一个master节点就行

1.3、Please enter all the source node IDs.

删除节点

这里我们输入 source节点,就是要迁移的源节点,随后输入 done。

1.4、Do you want to proceed with the proposed reshard plan (yes/no)? yes

解释:输入yes 同意上面的分配计划,并执行分配任务

image-1

迁移任务成功,此时我们去查看一下cluster nodes节点信息:

我们可以发现,10.211.55.12 的slot槽位已经被清空了,且状态由 master变成了slave。

cluster_nodes

2、删除节点

1
2
3
4
5
// 												集群模式	删除节点		删除节点的IP:PORT	节点唯一标识(在redis客户端可通过cluster nodes获取)
[root@s1 bin]# ./redis-cli --cluster del-node 10.211.55.12:6379 23c291df3e36b695c97b3304e1e357815da01400
>>> Removing node 8f4eb71b46bdccf2b13faa4d048740a5d10571da from cluster 10.211.55.13:6379
>>> Sending CLUSTER FORGET messages to the cluster...
>>> Sending CLUSTER RESET SOFT to the deleted node.

查看cluster nodes:

cluster_nodes

如图所示,节点已经删除成功,整个小集群都被删除了。

跳转重定向

  • 当客户端向节点发送了指令后,该节点发现指令的key不属于我的slot槽位,该节点就会发送一个跳转指令(携带正确的节点地址)告知客户端,客户端收到后就会进行重定向,并更新本地的槽位映射表缓存,后续所有key将使用新的缓存表。

一般会出现跳转重定向的场景

如果我们重新分配了节点的slot槽位,但是客户端还没有更新到最新槽位节点,就会出现 跳转重定向问题。

如:0-5460 原本属于 10.211.55.6,现在我们把槽位 改成0-1000 属于 10.211.55.6 ,1001-5460 属于10.211.55.7

而客户端发送的命令,经过算法计算后slot在2000,但客户端没有更新最新槽位,依然会发给10.211.5.6 ,10.211.5.6 就会发送最新的节点信息告知客户端,客户端在进行重定向并更新本地slot换存。

slot重定向

重定向

Java Jedis操作Redis Cluster

1、引入相关依赖

1
2
3
4
5
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>

2、Test Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@SpringBootTest
class SpringResitTemplateDemoApplicationTests {

@Test
void jedisCluster() throws IOException {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(20);
config.setMaxIdle(10);
config.setMinIdle(5);

Set<HostAndPort> jedisClusterNode = new HashSet<HostAndPort>();
jedisClusterNode.add(new HostAndPort("10.211.55.6", 6379));
jedisClusterNode.add(new HostAndPort("10.211.55.7", 6379));
jedisClusterNode.add(new HostAndPort("10.211.55.8", 6379));
jedisClusterNode.add(new HostAndPort("10.211.55.9", 6379));
jedisClusterNode.add(new HostAndPort("10.211.55.10", 6379));
jedisClusterNode.add(new HostAndPort("10.211.55.11", 6379));

JedisCluster jedisCluster = null;
try {
//connectionTimeout:指的是连接一个url的连接等待时间
//soTimeout:指的是连接上一个url,获取response的返回等待时间
jedisCluster = new JedisCluster(jedisClusterNode, 6000, 5000, 10, null, config);
System.out.println(jedisCluster.set("cluster", "123"));
System.out.println(jedisCluster.get("cluster"));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedisCluster != null)
jedisCluster.close();
}
}
}

分析Jedis源码

我们通过分析jedis的源码,可以大致的明白 jedis 是如何通过工具实现对key的CRC16算法计算和取模。

image-1 image-2 image-3

image-4

image-5

模拟Jedis进行CRC16算法后取模:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
System.out.println(JedisClusterCRC16.getSlot("test")%16384);
System.out.println(JedisClusterCRC16.getSlot("test") & (16384-1));
}

// 执行结果
6918
6918

Spring Boot整合Redis Cluser

1、引入相关依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.3.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>

2、yml配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

server:
port: 8080

spring:
redis:
database: 0
timeout: 3000
# password: 123123
# sentinel: #哨兵模式
# master: mymaster #哨兵master组名称
# nodes: 10.211.55.9:26379,10.211.55.10:26379,10.211.55.11:26379
cluster:
nodes: 10.211.55.6:6379,10.211.55.7:6379,10.211.55.8:6379,10.211.55.9:6379,10.211.55.10:6379,10.211.55.11:6379
lettuce:
pool:
max-idle: 50
min-idle: 10
max-active: 100
max-wait: 1000

3、controller代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@ResponseBody
public class TestController {

@Autowired
private StringRedisTemplate stringRedisTemplate;


@GetMapping("/test")
public void test(){
int i = 1;
while (true){
try {
stringRedisTemplate.opsForValue().set("test"+i,i+"");
System.out.println("test"+i +"存储成功");
i++;
Thread.sleep(1000);
} catch (Exception e) {
System.out.println("错误:"+e);
}
}
}
}

4、访问controller

运行结果

5、我们模拟 master挂掉,在看看集群架构在slave中选举master需要多久

image-1

很明显,只出现了2次连接失败,相比哨兵模式选举速度快了很多,很快就选举出主机了。

运行结果

6、结论

10.211.55.10 之前是slave从机,但是我们kill掉后master,他就选举成了新的主机。

10.211.55.6 之前是master主机,但是我们模拟kill掉后,再次连接上来它现在已经变成了从机了。

cluster nodes

Redis Cluster 集群原理

Redis Cluster 将所有数据划分为 16384 个 slots(槽位),每个节点负责其中一部分槽位。槽位的信息存储于每个节点中。

当 Redis Cluster 的客户端来连接集群时,它也会得到一份集群的槽位配置信息并将其缓存在客户端本地。这样当客户端要查找某个 key时,可以直接定位到目标节点。同时因为槽位的信息可能会存在客户端与服务器不一致的情况,还需要纠正机制来实现槽位信息的校验调整。

槽位定位算法

Cluster 默认会对 key 值使用 CRC16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。

HASH_SLOT = CRC16(key) mod 16384

Redis集群节点间的通信机制

redis cluster节点间采取gossip协议进行通信

  • 维护集群的元数据(集群节点信息,主从角色,节点数量,各节点共享的数据等)有两种方式:**集中式和gossip **

集中式

优点在于元数据的更新和读取,时效性非常好,一旦元数据出现变更立即就会更新到集中式的存储中,其他节点读取的时候立即就可以立即感知到;不足在于所有的元数据的更新压力全部集中在一个地方,可能导致元数据的存储压力。 很多中间件都会借助zookeeper集中式存储元数据。

gossip

0

gossip协议包含多种消息,包括ping,pong,meet,fail等等。

meet:某个节点发送meet给新加入的节点,让新节点加入集群中,然后新节点就会开始与其他节点进行通信;

ping:每个节点都会频繁给其他节点发送ping,其中包含自己的状态还有自己维护的集群元数据,互相通过ping交换元数据(类似自己感知到的集群节点增加和移除,hash slot信息等);

pong: 对ping和meet消息的返回,包含自己的状态和其他信息,也可以用于信息广播和更新;

fail: 某个节点判断另一个节点fail之后,就发送fail给其他节点,通知其他节点,指定的节点宕机了。

gossip协议的优点在于元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;缺点在于元数据更新有延时可能导致集群的一些操作会有一些滞后。

gossip通信的10000端口

通信示意图

每个节点都有一个专门用于节点间gossip通信的端口,就是自己提供服务的端口号+10000,比如6379,那么用于节点间通信的就是16379端口。 每个节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping消息之后返回pong消息。

集群超时时间

在生产环境中,网络波动是很常见的,波动期间可能会出现无法连接的情况,但是波动结束又会恢复正常。

为了避免波动让cluster误判机器故障了,cluste提供了一种选项:cluster-node-timeout 节点持续超时时间,只有节点超过该时间后,cluster才会认定为该节点故障,从而选举新的主节点并进行切换。

Redis集群选举原理

当slave发现自己的master变为FAIL状态时,便尝试进行Failover(故障转移),希望自己成为新的master。

由于挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程, 其过程如下:

  1. slave发现自己的master变为fail,就发送fail给其他节点,通知其他节点,master宕机了。
  2. slave将自己的小集群currentEpoch(选举周期,因为可能面临多次选举才能决定住谁来做master)加1,并对整个集群所有节点广播FAILOVER_AUTH_REQUEST(故障转移认证请求)信息。
  3. 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK(故障转移确认)
    1. 注意:在同一个集群的选举周期内,master只会先给收到的slave发送ack
      • redis 集群选举原理
  4. 尝试failover的slave收集master返回的FAILOVER_AUTH_ACK,如果slave收到超过一半master节点的ack,就变成新的master。
    • 如果同一轮选举内,多台尝试failover的slave收到一样的ack数量,则会重复上面的操作,继续完成下一轮选举
    • 之前说过,搭建集群最少需要3个master,如果只有2个master,当1个master挂了,只剩下一个master是无法完成选举的
  5. slave广播Pong消息通知其他集群的节点
    • Pong消息包含了自己的状态和自己的集群元数据(节点信息)

为了避免小集群下尝试failover的slave每个周期收到的ack数量都一样,redis通过 延时的机制来规避这个问题。每个slave发送的时间都不一样,就会有发送先后顺序。

延迟选举

slave节点并不是在master节点一进入 FAIL 状态就马上尝试发起选举,而是有一定延迟,一定的延迟确保我们等待FAIL状态在集群中传播,slave如果立即尝试选举,其它masters或许尚未意识到FAIL状态,可能会拒绝投票。

延迟计算公式:

DELAY = 500ms + random(0 ~ 500ms) + SLAVE_RANK * 1000ms

  • SLAVE_RANK表示此slave已经从master复制数据的总量的rank。Rank越小代表已复制的数据越新。这种方式下,持有最新数据的slave将会首先发起选举(理论上)。

    • 极端情况:redis只是ap架构(可用性),并不保证绝对一致性,因为网络问题,可能某些slave还没来得及收到同步数据,master就挂了。

同步断连引发的数据丢失问题

同步断连引发的数据丢失问题

客户端发送命令给master,master收到后存储在本地后会直接返回ack给客户端,并不会实时同步给slave节点。

那就很可能master刚保存数据,还没有来得及同步给其他slave节点,master就挂了,这样就会丢失部分数据。

集群脑裂引发的数据丢失问题

网络分区的解释:有时由于网络通讯故障,而不是服务器上的应用故障,导致一些节点认为master节点挂了,另外一些节点认为没挂。

  • redis集群没有过半机制会有脑裂问题,网络分区导致脑裂后多个主节点对外提供写服务,一旦网络分区恢复,会将其中一个主节点变为从节点,这时会有大量数据丢失。

举个例子:

redis脑裂问题

在一个小集群中,当master与slave出现了网络分区(slave与master之间连接故障,而client与master是正常的),slave在节点超时时间(cluster-node-timeout)结束后依旧连接不上master,slave会认为master挂了,slave会开始选举新的master。

注意:出现网络分区时,client往master发送数据,master收到数据后存储在本地后会立即发送ack告知client。当master与slave之间的线路恢复正常,此时就会存在2个master节点(一新一旧),旧的master节点状态就会变成slave并清空本地数据后同步新master的数据,那么,之前网络分区时 client存储在旧的master节点的数据都会丢失。

解决脑裂思路

解决脑裂的话,可以在redis配置里加上参数(这种方法不可能百分百避免数据丢失):

1
min-replicas-to-write 1 //写数据成功最少同步的slave数量,这个数量可以模仿大于半数机制配置,比如集群总共三个节点可以配置1,加上leader就是2,超过了半数

不开启该参数,client发送存储命令,master本地存储数据后会马上给client发送ack,随后在通过异步的形式把数据同步给slave。

一旦配置了该参数,如果client发送存储命令,master本地存储数据后必须马上同步给slave,并且slave必须达到配置要求,才会给client返回ack,如果slave达不到配置要求,就会返回写入失败。

image-2

注意:这个配置在一定程度上会影响集群的可用性,比如slave如果都挂了,这个集群master正常也不能对外提供服务了,因为你的slave都挂了,就无法达到配置要求,使用该参数应当慎重,评估具体场景权衡选择。

该参数是否解决了脑裂问题

image-3

当出现网络分区时,client发送存储数据给新的master,master会立即同步给左边的slave,并返回ack给client。

而老master和slave是断连的状态,client发送存储数据给老master,老master无法写入数据,就会返回写入失败给client。

注意:该参数并不是百分百完全解决脑裂问题,因为网络分区时,万一老master和部分master 连接正常呢?

在生产环境中是否开启该参数

设置该参数,会在一定程度上会影响集群的可用性,而我们在生产环境中更多的是追求redis集群的可用性,而并非是数据一致性。

集群是否完整才能对外提供服务

image-3

当redis.conf的配置cluster-require-full-coverage为no时,当其中的一个小集群master挂了后且所有的slave也挂了,其他的小集群依旧可以对外提供服务,如果为yes 则 整个集群都不可以对外提供服务。

Redis集群master节点限制

问题一:为什么集群中最少要存在3个master节点?

因为新master节点选举需要整个集群,超过一半以上的master同意(发送ack确认),才可以正常选举。如果只有2个master节点,是达不到选举条件的。

image-4

如图所示,集群中只有2个master节点,假如A集群的master挂了后,A集群内的slave发送故障转移通知给B集群的master节点,集群A的slave只能收到一个ACK,而slave必须收到一半以上的master ack(包括宕机的master) 才可以成为新的master。

问题二:为什么推荐集群master节点(小集群数量)为奇数

网上部分文章说搭建的小集群一定要奇数,实际上 偶数也是可以搭建正常使用的,只是以奇数去搭建性价比更高,更节省服务器资源。

3个master节点:

3个redis节点

如果搭建3个master节点,如果挂了一个master,还满足选举条件,挂两个master就无法满足选举条件,则无法满足选举条件,因为无法收集到一半(包含挂掉的小集群master节点)以上的master的ack的。

4个master节点:

4个redis节点

如果搭建4个master节点,如果挂了一个master,还满足选举条件。,如果挂了两个master,则无法满足选举条件。

从slave选举的角度来看,是毫无性价比可言,如果是为了 分担并发和存储更多的数据,还不如搭建5个master节点。

当然如果硬要搭建4个master,也是可以的,redis 并没有强制性约束。

5个master节点:

5个master节点

如果搭建5个master节点,即便挂了2台master,依然满足选举条件,这也是为什么 推荐以奇数来搭建master节点,从redis可用性来说性价比更高。

Redis集群对批量操作命令的支持

在redis集群上如果使用mset,mget等批量操作命令,会出现 不同的key slot不同的情况,导致执行命令批量失败,因为redis集群只支持所有key落在同一slot的情况。

如:mset name test age 18 ,cluster 通过Hash分片算法计算出name 和age的slot槽位不同,批量操作会全部执行失败,因为所有key必须落在同一个slot上。

解决方案

1
2
mset {user1}:1:name test {user1}:1:age 18
// redis只会用大括号里的 "user1" 做hash slot计算,所以算出来的slot值肯定相同,最后都能落在同一slot。