搭建主从集群
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
1. 主从集群结构
下图就是一个简单的Redis主从集群结构:
如图所示,集群中有一个master节点、两个slave节点(现在叫replica)。当我们通过Redis的Java客户端访问主从集群时,应该做好路由:
- 如果是写操作,应该访问master节点,master会自动将数据同步给两个slave节点
- 如果是读操作,建议访问各个slave节点,从而分担并发压力
2. 搭建主从集群
我们会在同一个虚拟机中利用3个Docker容器来搭建主从集群,容器信息如下:
2.1. 启动多个Redis实例
利用资料提供的docker-compose配置文件来构建主从集群:
a. 文件内容说明:
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003"]
- redis官网建议在搭建集群时, 使用host网络模式, 让每个容器直接使用宿主机的网络
- 此网络模式下, 容器会暴露在宿主机, 相当于成为宿主机的一个进程, 所以部署时也不需要端口映射
- 直接使用宿主机的端口就可以了
- entrypoint 入口配置, 用于修改容器的启动命令
- "--port" 参数用于配置容器的默认端口
b. 把镜像文件上传至root目录下, 然后加载镜像文件
c. 在虚拟机的root目录下新建redis目录, 上传配置文件包
d. 执行命令,运行集群
docker compose up -d
执行结果:
查看docker容器,发现都正常启动了:
由于采用的是host模式,我们看不到端口映射。不过能直接在宿主机通过ps命令查看到Redis进程:
2.2. 建立集群
虽然我们启动了3个Redis实例,但是它们并没有形成主从关系。我们需要通过命令来配置主从关系:
# 参数说明
# masterip 主节点IP
# masterport 主节点端口
# 两个命令都能用
# Redis5.0以前
slaveof <masterip> <masterport>
# Redis5.0以后
replicaof <masterip> <masterport>
有临时和永久两种模式:
- 永久生效:在redis.conf文件中利用
slaveof
命令指定master
节点 - 临时生效:直接利用redis-cli控制台输入
slaveof
命令,指定master
节点
我们测试临时模式,首先连接r2
,让其以r1
为master
# 连接r2
docker exec -it r2 redis-cli -p 7002
# 认r1主,也就是7001
slaveof 192.168.150.101 7001
# 退出连接
exit
然后连接r3
,让其以r1
为master
# 连接r3
docker exec -it r3 redis-cli -p 7003
# 认r1主,也就是7001
slaveof 192.168.150.101 7001
然后连接r1
,查看集群状态:
# 连接r1
docker exec -it r1 redis-cli -p 7001
# 查看集群状态
info replication
可以看到,当前节点r1:7001
的角色是master
,有两个slave与其连接:
slave0
:port
是7002
,也就是r2
节点slave1
:port
是7003
,也就是r3
节点
其中重要的信息有:
- master_replid: 主节点的唯一id
- offset=672: 偏移量
2.3. 测试
依次在r1
、r2
、r3
节点上执行下面命令:
set num 123
get num
- 只有在
r1
这个节点上可以执行set
命令(写操作), 其它两个节点只能执行get
命令(读操作)。 - 也就是说读写操作已经分离了。
主从同步原理
主从同步原理时序图
- 当主从第一次同步连接或断开重连时,从节点都会发送psync请求,尝试数据同步
- 主节点判断从节点是否第一次连接
- 每个节点默认都是主节点, 每个主节点都有唯一id属性replicationID, 简称replid
- 第一次paync, 从节点携带自己的replid
- 所以, 主节点只需要判断, 从节点的replid是否与自己一致就可以了
- 如果是第一次连接, 主节点把自己所有数据全部发送给子节点
- replid不一致, 属于第一次同步, 进行全量同步
- 主节点将完整内存数据生成
RDB
,发送到从节点 - 从节点清空本地数据, 加载RDBD到内存
- 从节点把自己的replid 改成 主节点的replid
- 如果是重新连接, 主节点把子节点缺少的数据发给子节点
- 全量同步需要先做RDB,然后将RDB文件通过网络传输个slave,成本太高了
- 大多数时候从节点与主节点都是做增量同步。
- 增量同步就是只更新主从节点存在差异的数据。
- 主节点会维护repl backlog文件, 其中会记录Redis处理过的命令及
offset
,包括主节点当前的offset
,和子节点已经拷贝到的offset
- 只要主从的offset一致, 代表数据一致, 如果offset存在差异, 那差异的部分,就是子节点需要增量拷贝的数据
repl_baklog
大小有上限,写满后会覆盖最早的数据。如果slave断开时间过久,导致尚未备份的数据被覆盖,则无法基于repl_baklog
做增量同步,只能再次全量同步。
- 每次主节点写数据时, 都把命令传播给子节点, 保持数据实时同步
主从集群优化
可以从以下几个方面来优化Redis主从集群:
- 在master中配置repl-diskless-syncyes启用无磁盘复制,避免全量同步时的磁盘IO。
- Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO, 一般建议不超过8G
- 适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
- 限制master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
哨兵原理
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。哨兵的具体作用如下:
- 监控: Sentinel会不断检查您的master和slave是否按预期工作
- 自动故障切换: 如果master故障,Sentinel会将-个slave提升为master。当故障实例恢复后也以新的master为主
- 通知: 当集群发生故障转移时,Sentinel会将最新节点角色信息推送给Redis的客户端
服务状态监控
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
- 主观下线: 如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
- 客观下线: 若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。
- quorum值最好超过Sentinel实例数量的一半
选举新的master
一旦发现master故障,sentinel需要在salve中选择一个作为新的master,选择依据是这样的:
- 首先会判断slave节点与master节点断开时间长短, 如果超过指定值 (down-after-milliseconds*10) 则会排除该slave节点
- 然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举, 默认都是1
- 如果slave-prority一样,则判断slave节点的offset值, 越大说明数据越新,优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高
如何实现故障转移
当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下:
- sentinel给备选的slave1节点发送 slaveof no one 命令,让该节点成为master
- sentinel给所有其它slave发送 slaveof 192.168.150.101 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
- 最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
搭建哨兵集群
首先, 停掉之前的redis集群:
# 老版本DockerCompose
docker-compose down
# 新版本Docker
docker compose down
然后,我们找到课前资料提供的sentinel.conf文件:
其内容如下:
sentinel announce-ip "192.168.150.101"
sentinel monitor hmaster 192.168.150.101 7001 2
sentinel down-after-milliseconds hmaster 5000
sentinel failover-timeout hmaster 60000
说明:
sentinel announce-ip "192.168.150.101"
:声明当前sentinel的ipsentinel monitor hmaster 192.168.150.101 7001 2
:指定集群的主节点信息
-
hmaster
:主节点名称,自定义,任意写192.168.150.101 7001
:主节点的ip和端口2
:认定master
下线时的quorum
值
sentinel down-after-milliseconds hmaster 5000
:声明master节点超时多久后被标记下线sentinel failover-timeout hmaster 60000
:在第一次故障转移失败后多久再次重试- 把配置文件中的信息修改为自己虚拟机的地址
我们在虚拟机的/root/redis
目录下新建3个文件夹:s1
、s2
、s3
:
- 将课前资料提供的
sentinel.conf
文件上传到s1文件夹中, 再拷贝到其他文件夹中。
- 接着修改
docker-compose.yaml
文件, 注意ip地址,内容如下:
version: "3.2"
services:
r1:
image: redis
container_name: r1
network_mode: "host"
entrypoint: ["redis-server", "--port", "7001"]
r2:
image: redis
container_name: r2
network_mode: "host"
entrypoint: ["redis-server", "--port", "7002", "--slaveof", "192.168.150.101", "7001"]
r3:
image: redis
container_name: r3
network_mode: "host"
entrypoint: ["redis-server", "--port", "7003", "--slaveof", "192.168.150.101", "7001"]
s1:
image: redis
container_name: s1
volumes:
- /root/redis/s1:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27001"]
s2:
image: redis
container_name: s2
volumes:
- /root/redis/s2:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27002"]
s3:
image: redis
container_name: s3
volumes:
- /root/redis/s3:/etc/redis
network_mode: "host"
entrypoint: ["redis-sentinel", "/etc/redis/sentinel.conf", "--port", "27003"]
- 直接运行命令,启动集群:
docker-compose up -d
- 运行结果:
我们以s1节点为例,查看其运行日志:
可以看到sentinel
已经联系到了7001
这个节点,并且与其它几个哨兵也建立了链接。哨兵信息如下:
27001
:Sentinel ID
是8e91bd24ea8e5eb2aee38f1cf796dcb26bb88acf
27002
:Sentinel ID
是5bafeb97fc16a82b431c339f67b015a51dad5e4f
27003
:Sentinel ID
是56546568a2f7977da36abd3d2d7324c6c3f06b8d
演示failover
接下来,我们演示一下当主节点故障时,哨兵是如何完成集群故障恢复(failover)的。
我们连接7001
这个master
节点,然后通过命令让其休眠60秒,模拟宕机:
# 连接7001这个master节点,通过sleep模拟服务宕机,60秒后自动恢复
docker exec -it r1 redis-cli -p 7001 DEBUG sleep 60
稍微等待一段时间后,会发现sentinel节点触发了failover
:
RedisTemplate连接哨兵集群
分为三步:
- 1)引入依赖
- 2)配置哨兵地址
- 3)配置读写分离
1.引入依赖
就是SpringDataRedis的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置哨兵地址
连接哨兵集群与传统单点模式不同,不再需要设置每一个redis的地址,而是直接指定哨兵地址:
spring:
redis:
sentinel:
master: hmaster # 集群名
nodes: # 哨兵地址列表
- 192.168.150.101:27001
- 192.168.150.101:27002
- 192.168.150.101:27003
3.配置读写分离
最后,还要配置读写分离,让java客户端将写请求发送到master节点,读请求发送到slave节点。定义一个bean即可:
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
这个bean中配置的就是读写策略,包括四种:
MASTER
:从主节点读取MASTER_PREFERRED
:优先从master
节点读取,master
不可用才读取slave
REPLICA
:从slave
节点读取REPLICA_PREFERRED
:优先从slave
节点读取,所有的slave
都不可用才读取master