主从集群
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
数据同步概念
Replication Id和offset
在从节点发起数据同步的请求中,有两个重要的属性:
Replication Id
:简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid。offset
:偏移量,随着记录在repl_baklog
中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
因此slave做数据同步,必须向master声明自己的replication id和offset,master才可以判断到底需要同步哪些数据。 当从节点发起主从同步请求时,主节点会判断从节点的replid是否一致,如果不一致,说明是第一次请求数据同步。
repl_baklog缓冲区
repl_baklog
缓冲区是主从同步的重要机制,主节点在生成RDB文件期间会将命令记录到这个环形缓冲区中。
该缓冲区用于增量同步,确保从节点的偏移量与主节点保持一致。然而,如果从节点宕机且重启时缓冲区的数据已被覆盖,从节点就无法通过缓冲区恢复全部数据,导致数据不一致。
第一次数据同步
- 从节点发起数据同步请求,请求中携带
replid
和offset
两个属性。 - 主节点需要判断
replid
是否与自己一致,如果不一致,说明从节点是第一次申请数据同步,主节点需要生成RDB文件并将RDB文件发送给从节点。由于生成RDB文件的过程是异步的,主节点同时会持续记录在生成RDB文件期间产生的所有命令。 - 从节点获取RDB文件后,清空本地数据并加载RDB文件进行数据同步。加载完成后,从节点获取缓存区的命令,执行命令同步数据。
全量同步
全量同步:是指主节点生成RDB文件并发送给从节点,从节点清空本地数据并加载RDB文件的过程。它通常发生在从节点首次连接到主节点或数据不一致的情况下。
全量同步的发生场景有两种:
- 从节点首次连接主节点: 当一个新的从节点第一次连接到主节点时,它没有任何数据副本。因此,需要进行全量同步来获取主节点的完整数据集。全量同步过程:
- 从节点发起数据同步请求,请求中携带
replid
和offset
两个属性。 - 主节点需要判断
replid
是否与自己一致,如果不一致,说明从节点是第一次申请数据同步,主节点需要生成RDB文件并将RDB文件发送给从节点 - 从节点获取RDB文件后,清空本地数据并加载RDB文件进行数据同步。
- 从节点发起数据同步请求,请求中携带
- 主节点缓冲区超出容量: 主节点的
repl_baklog
缓冲区大小有上限,写满后会覆盖最早的数据。如果从节点断开时间过久,导致尚未备份的数据被覆盖,则主节点不能基于缓冲区做数据同步,只能再次使用全量同步获取RDB的完整数据集。
全量同步过程:
- 从节点发起数据同步请求,请求中携带
replid
和offset
两个属性。 - 主节点需要判断
replid
是否与自己一致,如果不一致,说明从节点是第一次申请数据同步,主节点需要生成RDB文件并将RDB文件发送给从节点 - 从节点获取RDB文件后,清空本地数据并加载RDB文件进行数据同步。
增量同步
增量同步:是指主节点在RDB文件生成期间记录的所有命令(写操作)被存储在replication backlog
缓冲区中,并在全量同步完成后发送给从节点,从节点执行这些命令的过程。增量同步用于保持主从节点之间的数据一致性。
增量同步的发生场景有两种:
-
全量同步后的持续增量同步: 在从节点完成初次的全量同步之后,主节点和从节点之间需要保持数据一致性。从节点会不断接收主节点的增量数据以更新其自身的数据状态。
-
从节点宕机重启后的增量同步: 当从节点因为故障、宕机或其他原因暂时失联,然后重新启动并重新连接到主节点时,主节点会尝试通过增量同步来恢复数据同步的状态。
增量同步过程:
- 从节点发起数据同步请求,请求中携带
replid
和offset
两个属性。 - 主节点需要判断
replid
是否与自己一致,如果一致,说明从节点不是第一次申请数据同步了(即从节点之前进行了全量同步),主节点返回continue,允许从节点获取repl_baklog
缓冲区的命令 - 从节点持续获取缓存区的命令,执行命令同步数据。
优化策略
可以从以下几个方面来优化Redis主从集群:
- 在master中配置
repl-diskless-sync yes
启用无磁盘复制,避免全量同步时的磁盘I/O - Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘I/O
- 适当提高
repl_baklog
缓存区的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步 - 限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
哨兵机制
在主从集群中,slave节点即使宕机,也可以从master节点上恢复数据。然而,如果master节点宕机,即使master节点做了持久化处理,在其重启后虽然能够恢复部分数据,但在重启和故障恢复的过程中,仍然可能会丢失大量数据,这对系统来说是不可接受的。
因此,为了解决上述问题,在主从集群的基础上引入了哨兵机制。哨兵机制的核心作用是监控主从集群中的各个节点,并在检测到master节点宕机时,自动从slave节点中选举一个新的master节点。
哨兵(Sentinel)机制的作用:
- 服务状态监控: Sentinel会不断检查集群中的master和slave节点是否按预期工作
- 自动故障恢复: 如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
- 通知Redis客户端: Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线: 如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线: 若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
自动故障恢复
选举新master节点
当sentinel检测到master节点客观下线时,需要在集群中选择一个slave节点作为新的master,选择依据是这样的:
- 节点断开时间长短:首先会判断slave节点与master节点断开时间长短,如果超过指定值
down-after-milliseconds * 10
则会排除该slave节点 - 优先级判断:slave从节点有
slave-priority
参数,越小优先级越高,如果是0则永不参与选举 - 数据同步状态:如果从节点优先级一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
- 节点 ID 大小:最后是判断slave节点的运行id大小,越小优先级越高,
进行故障转移
当节点2(master节点)故障后,sentinel选举节点1为新的master节点,故障转移步骤如下:
- sentinel给备选的节点1发送slaveof no one命令,让节点1成为master
- sentinel给其它所有的slave节点发送slaveof 192.168.150.101 7002命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
- 最后,sentinel将故障节点标记为slave,当节点2恢复后会自动成为新的master的slave节点
通知Redis客户端
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,Redis的客户端必须感知这种变化及时更新连接信息。Spring的RedisTemplate底层利用lettuce
实现了节点的感知和自动切换。
- 在pom文件中引入redis的starter依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 然后在配置文件application.yml中指定sentinel相关信息
spring:
redis:
sentinel:
master: mymaster #指定master名称
nodes: #指定redis-sentinel集群信息
- 192.168.150.101:27001
- 192.168.150.101:27002
- 192.168.150.101:27003
- 配置读写分离
@Bean
public LettuceClientConfigurationBuilderCustomizer configurationBuilderCustomizer(){
return configBuilder -> configBuilder.readFrom(ReadFroM.REPLICA_PREFERRED);
}
这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:
- MASTER: 从主节点读取
- MASTER_PREFERRED: 优先从master节点读取,master不可用才读取replica
- REPLICA: 从slave(replica)节点读取
- REPLICA_PREFERRED: 优先从slave(replica)节点读取,所有的slave都不可用才读取master
分片集群
主从复制和哨兵机制虽然解决了Redis的高可用性和高并发读的问题,但仍然面临以下两个挑战:
- 海量数据存储的问题:单个Redis实例的内存和存储容量有限,无法处理海量数据。
- 高并发写入的问题:单个主节点在高并发写入的场景下容易成为性能瓶颈。
Redis中的分片集群(Sharded Cluster)是一种将数据分布在多个Redis节点上的方式。通过将数据水平分片,分片集群能够在数据量增加时提升集群的存储容量,同时将写入压力分散到多个master节点上,提升整体性能。
Redis 分片集群的核心作用:
- 数据水平扩展: 通过将数据分片存储在多个节点上,Redis 集群能够扩展到多个实例,以应对大规模数据存储和高并发请求。
- 负载均衡: 将请求均匀分布到不同的分片节点上,避免单点压力过大,确保系统性能的稳定性。
- 高可用性: 通过主从复制和自动故障恢复机制,Redis 集群能够在某个节点发生故障时,继续提供服务,确保系统的高可用性。
重要概念
散列插槽
Redis 集群通过哈希槽(Hash Slot)机制来分配数据到不同的分片节点上。整个哈希空间分为 16384 个槽,每个键根据其哈希值被分配到一个特定的槽中,而槽则由集群中的各个matser节点持有。
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值,分两种情况:
- 如果key中包含
{}
,且{}
中至少包含1个字符,{}
中的部分是有效部分 - 如果key中不包含
{}
,整个key都是有效部分
例如:key是num,那么就根据num计算,如果是{modox}num,则根据modox计算。计算方式是利用CRC16
算法得到一个hash值,然后对16384取余,得到的结果就是插槽的slot值。
Redis客户端如何进行数据访问? 根据键的哈希值确定数据所在的分片节点,然后直接与该节点通信。
如何将同一类数据保存在同一个Redis节点上? 只需设置一个统一的有效部分,如{shopId}
配置纪元
配置纪元的作用是标识和跟踪集群配置的版本,确保集群中的所有节点在主节点故障转移和配置变更时保持一致。
1.配置纪元是只增不减的整数:
- 每个主节点都有一个自身维护的配置纪元 (
clusterNode.configEpoch
),表示该主节点的版本。这个配置纪元是集群变更时用于标识和协调的关键因素。 - 每个主节点的配置纪元都不同,以确保集群内的节点可以正确识别和处理最新的配置变更。
2.从节点会复制主节点的配置纪元:
- 当从节点与其对应的主节点同步时,它会复制该主节点的配置纪元。这样在主节点发生故障时,从节点可以使用这个配置纪元参与选举并成为新的主节点。
3.全局配置纪元:
- 整个集群维护一个全局的配置纪元 (
clusterState.currentEpoch
),记录集群内所有主节点的配置纪元中的最大版本号。这个全局纪元会在集群发生关键事件(如故障转移、添加/删除节点)时增加,以确保集群状态的一致性。
4.选举时选择纪元数最大的从节点:
- 在故障转移过程中,集群会优先选择配置纪元最大的从节点作为新的主节点。因为这个从节点的数据更可能是最新的,并且它在选举中更有可能获得其他主节点的支持。
服务状态监控
Redis分片集群的各个节点通过ping/pong
进行消息通信,转播槽的信息和节点状态信息,故障发现也是通过这个动作实现的,类似于sentinel
,有主观下线和客观下线。
- 主观下线(PFAIL): 集群中的每个节点都会定期通过 PING-PONG 消息与其他节点通信。如果一个节点在指定时间内没有响应其他节点的 PING 请求,该节点会被标记为主观下线。
- 客观下线(FAIL): 如果多个节点都将同一个节点标记为 PFAIL,那么通过投票机制,该节点将被标记为客观下线(FAIL)。这个状态会在集群中广播,所有节点都认同该节点已不可用。
故障恢复
选举新的master节点
Redis 分片集群和 Sentinel 机制在选举新的 master 节点时规则基本相同,唯一的区别在于节点断开时间的处理方式不同。
- 节点断开时间长短:每个从节点检查与故障主节点的断线时间,断开时间超过
cluster-node-timeout * cluster-slave-validity-factor
则取消资格。cluster-slave-validity-factor : 默认是10 - 优先级判断:slave从节点有
slave-priority
参数,越小优先级越高,如果是0则永不参与选举 - 数据同步状态:如果从节点优先级一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
- 配置纪元: 在故障转移过程中,集群会优先选择配置纪元最大的从节点作为新的主节点。因为配置纪元越大的从节点,数据更可能是新的。
进行故障转移
当新的 master 节点选举完成后,Redis 集群会自动进行故障转移,具体包括以下步骤:
- 提升新的 master 节点:Redis 集群通过内部命令
SLAVEOF NO ONE
将选中的从节点提升为新的 master 节点。 - 更新哈希槽映射:Redis 集群会自动更新哈希槽与节点的映射关系,新的 master 节点将执行
CLUSTER DELSLOTS
操作撤销故障主节点负责的槽,并执行CLUSTER ADDSLOTS
把这些槽委派给自己。 - 重新配置和广播:Redis 集群将剩余的从节点重新配置为新 master 节点的从节点,并广播新的 master 信息给所有节点,确保集群内所有节点都更新哈希槽映射,并将新 master 的信息同步到其他节点。
- 节点重连:如果故障的 master 节点恢复上线,它通常会被重新配置为新的 master 的从节点,并同步数据以确保与新 master 保持数据一致性。
通知Redis客户端
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致。
- 引入redis的starter依赖
- 配置分片集群地址
- 配置读写分离
与哨兵模式相比,只有yaml配置文件的配置方式存在差异,如下:
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
集群伸缩
Redis 集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容,对节点进行灵活上下线控制,原理可抽象为槽和对应数据在不同节点之间灵活移动。
集群扩容
1.添加节点:
Redis分片集群提供了为现有集群添加新节点的,命令如下:
redis-cli --cluster add-node new_host:new_port existing_host:existing_port
--cluster-slave
--cluster-master-id <arg>
如果需要直接指定新节点为某一master的从节点,可使用如下命令:
redis-cli --cluster add-node <新节点IP>:<端口> <现有节点IP>:<端口> --cluster-slave --cluster-master-id <主节点ID>
2.迁移插槽:
可通过reshard
命令将当前节点的散列插槽分配给其他节点。
redis-cli --cluster reshard <当前节点IP>:<端口>
接着Redis会提示需要移动多少插槽,自行输入即可。
How many slots do you want to move (from 1 to 16384)?
然后需要输入接收插槽的节点ID,确认后即可实现插槽的迁移
What is the receiving node ID?
3.添加从节点:
由于新的master节点相比其他主节点目前还没有从节点,因此该节点不具备故障转移的能力。
可以在从节点下使用cluster replicate
命令为主节点添加对应从节点(在分片集群下slaveof命令添加从节点的操作不再支持)。
cluster replicate <主节点ID>
从节点内部除了对主节点发起全量复制之外,还需要更新本地节点的集群相关状态。
集群缩容
1.迁移插槽:
缩容操作需要非常谨慎,因为如果下线的节点持有插槽,直接删除可能会引起数据一致性问题,因此需要将槽迁移给其他节点后才能安全下线,流程同上。
redis-cli --cluster reshard <当前节点IP>:<端口>
接着Redis会提示需要移动多少插槽,自行输入即可。
How many slots do you want to move (from 1 to 16384)?
然后需要输入接收插槽的节点ID,确认后即可实现插槽的迁移
What is the receiving node ID?
2.忘记节点:
在一个可用的节点上执行删除节点的命令:
redis-cli --cluster del-node <可用的节点IP>:<可用的节点端口> <需要删除的节点ID>
手动故障转移
在 Redis 集群中,手动故障转移允许管理员主动介入,以便在发现主节点故障时,迅速将其替换为副本节点,确保系统的持续可用性和稳定性。
此外,手动数据迁移还支持三种不同模式:
- 缺省: 默认的流程,如图1~6步
- force: 省略了对offset的一致性校验
- takeover: 直接执行第5步,忽略数据一致性、忽略master状态和其它master的意见