03-Redis哨兵集群实现
一、简介

**Sen**tinel
(哨兵)进程是用于监控Redis
集群中Master
主服务器工作的状态,在Master
主服务器发生故障的时候,可以实现Master
和Slave
服务器的切换,保证系统的高可用。
哨兵(Sentinel
) 是一个分布式系统,你可以在一个架构中运行多个哨兵(sentinel
) 进程,这些进程使用流言协议来接收关于Master
主服务器是否下线的信息,并使用投票协议来决定是否执行自动故障迁移,以及选择哪个Slave
作为新的Master
。
1.1 流言协议
流言协议是一种去中心化的信息传播方式,其工作原理类似于人们在日常生活中传播流言:每个节点都将自己所知的信息告诉给它所连接的其他节点,这些节点再将信息传播给它们所连接的节点,以此类推,直到所有节点都获得了这条信息。
1.2 Redis哨兵集群角色划分
- 主节点(
**Master**
): 处理客户端的读写请求。 - 从节点(
**Slave**
): 复制主节点的数据,用于提供读取服务和备份。 - 哨兵节点(
**Sentinel**
): 监控集群中各节点的健康状态,负责选举和故障转移。
1.3 哨兵作用
- 监控(
Monitoring
): 哨兵(sentinel
) 会不断地检查你的Master
和Slave
是否运作正常。 - 提醒(
Notification
):当被监控的某个Redis
节点出现问题时, 哨兵(sentinel
) 可以通过API
向管理员或者其他应用程序发送通知。 - 自动故障迁移(
Automatic failover
):当一个Master
不能正常工作时,哨兵(sentinel
) 会开始一次自动故障迁移操作,它会将失效Master
的其中一个Slave
升级为新的Master
, 并让失效Master
的其他Slave
改为复制新的Master
;当客户端试图连接失效的Master
时,集群也会向客户端返回新Master
的地址,使得集群可以使用现在的Master
替换失效Master
。
1.4 哨兵之间通信

在上图图中,哨兵 1 把自己的 IP(172.16.19.3)和端口(26579)发布到“sentinel:hello
”频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。
然后,哨兵 2、3 可以和哨兵 1 建立网络连接。通过这个方式,哨兵 2 和 3 也可以建立网络连接,这样一来,哨兵集群就形成了。它们相互间可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。
哨兵实例之间可以相互发现,要归功于 Redis
提供的 pub/sub 机制,也就是发布 / 订阅机制。
哨兵只要和主库建立起了连接,就可以在主库上发布消息了,比如说发布它自己的连接信息(IP 和端口)。同时,它也可以从主库上订阅消息,获得其他哨兵发布的连接信息。当多个哨兵实例都在主库上做了发布和订阅操作后,它们之间就能知道彼此的 IP
地址和端口。
为了区分不同应用的消息,Redis
会以频道的形式,对这些消息进行分门别类的管理。所谓的频道,实际上就是消息的类别。当消息类别相同时,它们就属于同一个频道。反之,就属于不同的频道。只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。
在主从集群中,主库上有一个名为“sentinel:hello”
的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
二、哨兵集群实现原理
- 每个
Sentinel
(哨兵)每隔一秒向整个集群中的Master
主服务器,Slave
从服务器以及其他Sentinel
(哨兵)进程发送一个 PING 命令。并通过实例返回的结果来判断实例是否在线。 - 如果一个实例(
instance
)距离最后一次有效回复 PING 命令的时间超过down-after-milliseconds
选项所指定的值, 则这个实例会被Sentinel
(哨兵)进程标记为主观下线(SDOWN
)。 - 如果一个
Master
主服务器被标记为主观下线(SDOWN
),则正在监视这个Master
主服务器的所有Sentinel
(哨兵)进程以每秒一次的频率确认Master
主服务器的确进入了主观下线状态。 - 当有足够数量的
Sentinel
(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master
主服务器进入了主观下线状态(SDOWN
), 则Master主服务器会被标记为客观下线(ODOWN
)。 - 若没有足够数量的
Sentinel
(哨兵)进程同意Master
主服务器下线,Master
主服务器的客观下线操作就会被移除。若Master
主服务器重新向Sentinel
(哨兵)进程发送PING
命令返回有效回复,Master
主服务器的主观下线状态就会被移除。
三、主观下线和客观下线
主观下线:Subjectively Down
,简称 SDOWN
,指的是当前一个Sentinel
实例对某个redis
服务器做出的下线判断。
客观下线:Objectively Down
, 简称ODOWN
,指的是多个 Sentinel
实例在对Master
做出 SDOWN
判断,并且通过SENTINEL is-master-down-by-addr
命令互相交流之后,得出的Master Server
下线判断,然后开启failover
故障转移
四、Redis 哨兵集群实现
由于 Redis
哨兵基于 Redis
主从集群基础上实现的。配置之前需要保证 Redis
主从集群的可用性。
4.1 集群规划
主机 IP | 角色 |
---|---|
192.168.174.50 | redis-master |
192.168.174.51 | redis-slave01 |
192.168.174.52 | redis-slave02 |
以上主机之间已实现
Redis
主从架构。
4.2 三台 Redis
节点修改哨兵配置
# Step 1:三台主机切换工作目录
$ cd /usr/local/redis/# Step 2:三台主机修改 sentinel.conf 哨兵配置文件
$ sentinel.conf
...
sentinel monitor mymaster 192.168.174.48 6379 2
sentinel down-after-milliseconds mymaster 3000
sentinel failover-timeout mymaster 10000
protected-mode no
...
参数解释:
sentinel monitor mymaster 192.168.174.48 6379 2
:当集群中有2个sentinel
认为master
死了时,才能真正认为该master
已经不可用了。 (slave
上面写的是master
的ip
,master
写自己ip
)
sentinel down-after-milliseconds mymaster 3000
: 表示如果名为 mymaster
的主节点在3秒(3000毫秒)内未对 Sentinel
的 PING
命令做出有效响应,那么 Sentinel
会开始考虑该主节点可能已经出现故障,并做好相应的故障转移准备。
sentinel failover-timeout mymaster 10000
:表示在进行名为 mymaster
的主节点的故障转移操作时,Sentinel
最多允许花费10秒(10000毫秒)的时间来完成整个操作。
protected-mode no
:关闭加密保护模式
# Step 3:切换工作目录
$ cd /usr/local/redis/src/
# Step 4:三台主机启动sentinel
$ ./src/redis-sentinel sentinel.conf
注意:在生产环境下将哨兵模式启动放到后台执行: ./src/redis-sentinel sentinel.conf &
redis-master
执行结果回显:
redis-slave01
执行结果回显:
redis-slave02
执行结果回显:
五、哨兵集群可用性验证
实施步骤:将主节点上的 sentinel
关闭,然后再关闭 redis
服务。关闭之后,观察 master
角色地址是否切换。redis-slave01
终端回显如下:
实施步骤:将主节点上 Redis
及 sentinel
重新启动,观察是否能够重新加入哨兵集群。切记,一定先启动 Redis
后再启动 Sentinel
。redis-master
启动完成后,终端回显如下:
六、扩展内容
6.1 哨兵如何知道从库的ip地址和端口

由哨兵向主库发送 INFO 命令来完成的。就像上图所示,哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。
6.2 Redis
故障Master
选举算法
- 优先级: 每个节点都有一个优先级,选择优先级最高的节点作为新的主节点。
- 复制偏移量: 选择复制偏移量最大的从节点,确保数据同步性。
- 运行ID: 选择运行ID最大的节点,确保节点唯一性。
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.exceptions.JedisException;import java.util.Set;public class RedisClusterElection {public static void main(String[] args) {String masterName = "mymaster";Set<String> sentinels = Set.of("sentinel1:26379", "sentinel2:26379", "sentinel3:26379");try (JedisSentinelPool sentinelPool = new JedisSentinelPool(masterName, sentinels)) {Jedis jedis = sentinelPool.getResource();// 获取当前主节点String currentMaster = jedis.sentinelGetMasterAddrByName(masterName).getHost();System.out.println("Current Master: " + currentMaster);// 模拟主节点故障simulateMasterFailure(jedis, masterName);// 等待哨兵节点进行选举Thread.sleep(5000);// 获取新的主节点String newMaster = jedis.sentinelGetMasterAddrByName(masterName).getHost();System.out.println("New Master: " + newMaster);} catch (JedisException | InterruptedException e) {e.printStackTrace();}}private static void simulateMasterFailure(Jedis jedis, String masterName) {// 模拟主节点故障,停止主节点jedis.sentinelFailover(masterName);}
}