redis 分片集群

主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:海量数据存储问题、高并发写的问题。为了解决这俩问题,Redis 提供了分片集群的方案。

搭建分片集群

具体搭建流程参考课前资料 《Redis集群方案.md 》,结构如图:

image-20210725155747294

分片集群特征:

  • 集群中有多个 master,每个master保存不同数据
  • 每个 master 都可以有多个 slave 节点
  • master 之间通过 ping 监测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

Redis 集群配置参数

我们即将创建一个示例集群部署。在继续之前,我们先介绍一下文件中 Redis Cluster 引入的配置参数redis.conf

  • **cluster-enabled<yes/no>**:如果是,则在特定 Redis 实例中启用 Redis Cluster 支持。否则,实例将像往常一样作为独立实例启动。
  • **cluster-config-file**:请注意,尽管有此选项的名称,但这不是用户可编辑的配置文件,而是 Redis Cluster 节点在每次发生更改时自动持久化集群配置(基本上是状态)的文件,为了能够在启动时重新读取它。该文件列出了集群中的其他节点、它们的状态、持久变量等内容。由于某些消息接收,此文件通常会被重写并刷新到磁盘上。
  • **cluster-node-timeout**:Redis 集群节点不可用的最长时间,而不被视为失败。如果主节点在超过指定的时间内无法访问,它将由其副本进行故障转移。此参数控制 Redis Cluster 中的其他重要内容。值得注意的是,在指定时间内无法到达大多数主节点的每个节点都将停止接受查询。
  • **cluster-slave-validity-factor*:如果设置为零,则副本将始终认为自己有效,因此将始终尝试对主节点进行故障转移,无论主节点和副本之间的链接保持断开连接的时间长短。如果该值为正,则计算最大断开时间作为节点超时*值乘以此选项提供的因子,如果节点是副本,则如果主链接断开连接的时间超过指定的时间,它将不会尝试启动故障转移。例如,如果节点超时设置为 5 秒,有效性因子设置为 10,则与主节点断开连接超过 50 秒的副本将不会尝试对其主节点进行故障转移。请注意,如果没有能够对其进行故障转移的副本,则任何非零值都可能导致 Redis 集群在主故障后不可用。在这种情况下,只有当原始主节点重新加入集群时,集群才会恢复可用。
  • **cluster-migration-barrier**:master 将保持连接的最小副本数,以便另一个副本迁移到不再被任何副本覆盖的 master。有关更多信息,请参阅本教程中有关副本迁移的相应部分。
  • **cluster-require-full-coverage<yes/no>**:如果设置为 yes,默认情况下,如果任何节点未覆盖某个百分比的键空间,则集群将停止接受写入。如果该选项设置为 no,即使只能处理有关键子集的请求,集群仍将提供查询服务。
  • **cluster-allow-reads-when-down<yes/no>**:如果设置为 no,默认情况下,当集群被标记为失败时,Redis 集群中的节点将停止提供所有流量,或者当节点无法访问时达到法定人数或未达到完全覆盖时。这可以防止从不知道集群更改的节点读取可能不一致的数据。可以将此选项设置为 yes 以允许在故障状态期间从节点读取,这对于希望优先考虑读取可用性但仍希望防止写入不一致的应用程序很有用。当使用只有一个或两个分片的 Redis 集群时,它也可以使用,因为它允许节点在主节点失败但无法自动故障转移时继续提供写入服务

Redis 集群数据分片

散列插槽

Redis Cluster 不使用一致性散列,而是使用不同形式的分片,其中每个键在概念上都是我们所谓的散列插槽的一部分。

Redis 集群中有 16384 个哈希槽,要计算给定键的哈希槽,我们只需将键的 CRC16 取模 16384。

Redis 集群中的每个节点都负责哈希槽的子集,因此,例如,您可能有一个具有 3 个节点的集群,其中:

  • 节点 A 包含从 0 到 5500 的哈希槽。
  • 节点 B 包含从 5501 到 11000 的哈希槽。
  • 节点 C 包含从 11001 到 16383 的哈希槽。

这使得添加和删除集群节点变得容易。例如,如果我想添加一个新节点 D,我需要将节点 A、B、C 的一些哈希槽移动到 D。同样,如果我想从集群中移除节点 A,我可以只移动这些哈希槽由 A 到 B 和 C 提供服务。一旦节点 A 为空,我可以将它完全从集群中删除。

将哈希槽从一个节点移动到另一个节点不需要停止任何操作;因此,添加和删除节点,或更改节点持有的哈希槽的百分比,不需要停机。

Redis Cluster 支持多键操作,只要单个命令执行(或整个事务,或 Lua 脚本执行)涉及的所有键都属于同一个哈希槽。用户可以通过使用称为哈希标签的功能强制多个键成为同一个哈希槽的一部分。

Keys hash tags

数据 key 不是与节点绑定,而是与插槽绑定。redis 会根据 key 的有效部分计算插槽值,分两种情况:

  • key中包含”{}”,且“{}”中至少包含1个字符,“{}”中的部分是有效部分
  • key中不包含“{}”,整个key都是有效部分

例如:

  • {user1000}.following和{user1000}.followers这两个key会被hash到相同的hash slot中,因为只有user1000会被用来计算hash slot值。
  • foo{}{bar}这个key不会启用hash tag因为第一个{和}之间没有字符。
  • foozap这个key中的{bar部分会被用来计算hash slot
  • foo{bar}{zap}这个key中的bar会被用来计算计算hash slot,而zap不会

Redis Cluster 主从模型

为了在主节点的子集出现故障或无法与大多数节点通信时保持可用,Redis 集群使用主副本模型,其中每个哈希槽具有 1(主节点本身)到 N 个副本(N-1额外的副本节点)。

在我们具有节点 A、B、C 的示例集群中,如果节点 B 发生故障,集群将无法继续,因为我们不再有办法为 5501-11000 范围内的哈希槽提供服务。

但是,在创建集群时(或稍后),我们为每个 master 添加一个副本节点,这样最终的集群由作为 master 节点的 A、B、C 和作为主节点的 A1、B1、C1 组成。副本节点。这样,如果节点 B 发生故障,系统可以继续运行。

节点 B1 复制 B,而 B 发生故障,集群会将节点 B1 提升为新的 master,并继续正常运行。

但是要注意,如果节点 B 和 B1 同时出现故障,Redis Cluster 将无法继续运行。

Cluster nodes属性

每个节点在cluster中有一个唯一的名字。这个名字由160bit随机十六进制数字表示,并在节点启动时第一次获得(通常通过/dev/urandom)。节点在配置文件中保留它的ID,并永远地使用这个ID,直到被管理员使用CLUSTER RESET HARD命令hard reset这个节点。

节点ID被用来在整个cluster中标识每个节点。一个节点可以修改自己的IP地址而不需要修改自己的ID。Cluster可以检测到IP /port的改动并通过运行在cluster bus上的gossip协议重新配置该节点。

节点ID不是唯一与节点绑定的信息,但是他是唯一的一个总是保持全局一致的字段。每个节点都拥有一系列相关的信息。一些信息时关于本节点在集群中配置细节,并最终在cluster内部保持一致的。而其他信息,比如节点最后被ping的时间,是节点的本地信息。

每个节点维护着集群内其他节点的以下信息:node id, 节点的IP和port节点标签master node id(如果这是一个slave节点),最后被挂起的ping的发送时间(如果没有挂起的ping则为0),最后一次收到pong的时间当前的节点configuration epoch链接状态,以及最后是该节点服务的hash slots

故障恢复(Failover)

master节点挂了之后,如何进行故障恢复呢?

当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave。Failover的过程需要经过类Raft协议的过程在整个集群内达到一致, 其过程如下:

  • slave发现自己的master变为FAIL
  • 将自己记录的集群currentEpoch加1,并广播Failover Request信息
  • 其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
  • 尝试failover的slave收集FAILOVER_AUTH_ACK
  • 超过半数后变成新Master
  • 广播Pong通知其他集群节点

集群伸缩

redis-cli –cluster提供了很多操作集群的命令,可以通过下面方式查看:

image-20210725160138290

比如,添加节点的命令:

image-20210725160448139

需求分析

需求:向集群中添加一个新的master节点,并向其中存储 num = 10

  • 启动一个新的redis实例,端口为7004
  • 添加7004到之前的集群,并作为一个master节点
  • 给7004节点分配插槽,使得num这个key可以存储到7004实例

这里需要两个新的功能:

  • 添加一个节点到集群中
  • 将部分插槽分配到新插槽

创建新的redis实例

创建一个文件夹:

1
mkdir 7004

拷贝配置文件:

1
cp redis.conf /7004

修改配置文件:

1
sed /s/6379/7004/g 7004/redis.conf

启动

1
redis-server 7004/redis.conf

添加新节点到redis

添加节点的语法如下:

image-20210725160448139

执行命令:

1
redis-cli --cluster add-node  192.168.150.101:7004 192.168.150.101:7001

通过命令查看集群状态:

1
redis-cli -p 7001 cluster nodes

如图,7004加入了集群,并且默认是一个master节点:

image-20210725161007099

但是,可以看到7004节点的插槽数量为0,因此没有任何数据可以存储到7004上

转移插槽

我们要将num存储到7004节点,因此需要先看看num的插槽是多少:

image-20210725161241793

如上图所示,num的插槽为2765.

我们可以将0~3000的插槽从7001转移到7004,命令格式如下:

image-20210725161401925

具体命令如下:

建立连接:

image-20210725161506241

得到下面的反馈:

image-20210725161540841

询问要移动多少个插槽,我们计划是3000个:

新的问题来了:

image-20210725161637152

那个node来接收这些插槽??

显然是7004,那么7004节点的id是多少呢?

image-20210725161731738

复制这个id,然后拷贝到刚才的控制台后:

image-20210725161817642

Source node 表示源节点,这里询问,你的插槽是从哪里移动过来的?

  • all:代表全部,也就是三个节点各转移一部分
  • 具体的id:目标节点的id
  • done:结束

这里我们要从7001获取,因此填写7001的id:

image-20210725162030478

填完后,点击done,这样插槽转移就准备好了:

image-20210725162101228

确认要转移吗?输入yes:

然后,通过命令查看结果:

image-20210725162145497

可以看到:

image-20210725162224058

目的达成。

要删除副本节点,只需使用del-noderedis-cli 的命令:

1
redis-cli --cluster del-node 127.0.0.1:7000 `<node-id>`

第一个参数只是集群中的一个随机节点,第二个参数是要删除的节点的 ID。

您也可以以相同的方式删除主节点,但是要删除主节点,它必须为空。如果主节点不为空,您需要将数据从它重新分片到之前的所有其他主节点。

删除主节点的另一种方法是在其一个副本上对其执行手动故障转移,并在该节点变成新主节点的副本后删除该节点。显然,当您想减少集群中的实际 master 数量时,这无济于事,在这种情况下,需要重新分片。

RedisTemplate访问分片集群

RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:

1)引入redis的starter依赖

2)配置分片集群地址

3)配置读写分离

与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:

1
2
3
4
5
6
7
8
9
10
spring:
redis:
cluster:
nodes:
- 192.168.150.101:7001
- 192.168.150.101:7002
- 192.168.150.101:7003
- 192.168.150.101:8001
- 192.168.150.101:8002
- 192.168.150.101:8003