Redis 哨兵模式
2026/3/20大约 22 分钟
Redis 哨兵模式
一、Sentinel 概述
1.1 为什么需要 Sentinel
主从复制解决了数据备份和读扩展的问题,但存在以下不足:
- 主节点故障需要手动处理:管理员需要手动选择从节点、提升为主节点、通知应用
- 故障转移时间长:手动处理可能需要数分钟甚至更长
- 缺乏自动监控:无法自动检测节点状态
Sentinel(哨兵)是 Redis 官方的高可用解决方案,提供:
- 监控(Monitoring):持续检查主从节点是否正常工作
- 通知(Notification):节点故障时通过 API 通知管理员
- 自动故障转移(Automatic Failover):主节点故障时自动将从节点提升为主节点
- 配置提供者(Configuration Provider):客户端通过 Sentinel 获取当前主节点地址
1.2 Sentinel 架构
二、Sentinel 部署配置
2.1 环境准备
假设我们有以下节点:
| 角色 | IP | Redis 端口 | Sentinel 端口 |
|---|---|---|---|
| Master | 192.168.1.10 | 6379 | 26379 |
| Slave 1 | 192.168.1.11 | 6379 | 26379 |
| Slave 2 | 192.168.1.12 | 6379 | 26379 |
2.2 Redis 主从配置
主节点配置(192.168.1.10)
# redis.conf
bind 0.0.0.0
port 6379
daemonize yes
pidfile /var/run/redis_6379.pid
logfile /var/log/redis/redis.log
dir /data/redis
# 密码
requirepass redis_password
masterauth redis_password # 故障转移后可能成为从节点,需要配置
# 持久化
appendonly yes
appendfsync everysec
从节点配置(192.168.1.11、192.168.1.12)
# redis.conf
bind 0.0.0.0
port 6379
daemonize yes
pidfile /var/run/redis_6379.pid
logfile /var/log/redis/redis.log
dir /data/redis
# 密码
requirepass redis_password
masterauth redis_password
# 复制配置
replicaof 192.168.1.10 6379
# 持久化
appendonly yes
appendfsync everysec
2.3 Sentinel 配置
每个节点上都部署一个 Sentinel,配置文件 sentinel.conf:
# sentinel.conf
# 绑定地址
bind 0.0.0.0
# 端口
port 26379
# 后台运行
daemonize yes
# 日志文件
logfile /var/log/redis/sentinel.log
# 工作目录
dir /data/redis
# Sentinel 密码(可选,Redis 6.2+)
# requirepass sentinel_password
# =========================
# 监控的主节点配置
# =========================
# sentinel monitor <master-name> <ip> <port> <quorum>
# quorum: 判定主节点故障需要的 Sentinel 数量
sentinel monitor mymaster 192.168.1.10 6379 2
# 主节点密码
sentinel auth-pass mymaster redis_password
# 主节点多久无响应被判定为主观下线(毫秒)
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时时间(毫秒)
sentinel failover-timeout mymaster 180000
# 同时进行同步的从节点数量
sentinel parallel-syncs mymaster 1
# =========================
# 通知脚本(可选)
# =========================
# 故障转移时执行的脚本
# sentinel notification-script mymaster /scripts/notify.sh
# 故障转移完成后执行的脚本
# sentinel client-reconfig-script mymaster /scripts/reconfig.sh
2.4 启动服务
# 1. 启动所有 Redis 节点
redis-server /etc/redis/redis.conf
# 2. 验证主从关系
redis-cli -h 192.168.1.10 -a redis_password INFO replication
# 3. 启动所有 Sentinel
redis-sentinel /etc/redis/sentinel.conf
# 或者
redis-server /etc/redis/sentinel.conf --sentinel
# 4. 验证 Sentinel 状态
redis-cli -p 26379 INFO sentinel
# sentinel_masters:1
# master0:name=mymaster,status=ok,address=192.168.1.10:6379,slaves=2,sentinels=3
2.5 Systemd 服务配置
# /etc/systemd/system/redis-sentinel.service
[Unit]
Description=Redis Sentinel
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/bin/redis-sentinel /etc/redis/sentinel.conf
ExecStop=/usr/local/bin/redis-cli -p 26379 shutdown
Restart=always
User=redis
Group=redis
[Install]
WantedBy=multi-user.target
三、Sentinel 工作原理
3.1 节点发现
Sentinel 启动后会自动发现其他 Sentinel 和从节点:
3.2 心跳检测
3.3 主观下线与客观下线
3.4 Sentinel 领导者选举
当主节点被判定为客观下线后,需要选举一个 Sentinel 来执行故障转移:
3.5 故障转移流程
四、Sentinel 命令
4.1 查看信息
# 连接 Sentinel
redis-cli -p 26379
# 查看 Sentinel 监控的主节点
SENTINEL masters
# 查看指定主节点信息
SENTINEL master mymaster
# 查看主节点的从节点
SENTINEL slaves mymaster
# 或(Redis 5.0+)
SENTINEL replicas mymaster
# 查看其他 Sentinel
SENTINEL sentinels mymaster
# 获取主节点地址
SENTINEL get-master-addr-by-name mymaster
# 返回:
# 1) "192.168.1.10"
# 2) "6379"
# 检查主节点是否可达
SENTINEL ckquorum mymaster
4.2 运维命令
# 重置主节点状态(清除从节点和 Sentinel 列表)
SENTINEL reset mymaster
# 手动触发故障转移
SENTINEL failover mymaster
# 移除主节点
SENTINEL remove mymaster
# 添加监控的主节点
SENTINEL monitor mymaster 192.168.1.20 6379 2
# 修改配置
SENTINEL set mymaster down-after-milliseconds 60000
SENTINEL set mymaster failover-timeout 300000
SENTINEL set mymaster parallel-syncs 2
# 刷新配置
SENTINEL flushconfig
4.3 Pub/Sub 事件
Sentinel 通过 Pub/Sub 发布事件,客户端可以订阅获取状态变化:
# 订阅所有事件
redis-cli -p 26379 PSUBSCRIBE *
# 常见事件:
# +sdown # 主观下线
# -sdown # 取消主观下线
# +odown # 客观下线
# -odown # 取消客观下线
# +switch-master # 主节点切换
# +slave # 发现新从节点
# +sentinel # 发现新 Sentinel
# +failover-state-* # 故障转移状态变化
五、客户端连接
5.1 Jedis 连接 Sentinel
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisSentinelPool;
import java.util.HashSet;
import java.util.Set;
public class SentinelExample {
public static void main(String[] args) {
// Sentinel 节点地址
Set<String> sentinels = new HashSet<>();
sentinels.add("192.168.1.10:26379");
sentinels.add("192.168.1.11:26379");
sentinels.add("192.168.1.12:26379");
// 主节点名称
String masterName = "mymaster";
// 创建连接池
JedisSentinelPool pool = new JedisSentinelPool(
masterName,
sentinels,
"redis_password" // Redis 密码
);
// 使用连接
try (Jedis jedis = pool.getResource()) {
jedis.set("key", "value");
String value = jedis.get("key");
System.out.println("Value: " + value);
}
// 关闭连接池
pool.close();
}
}
带完整配置的示例:
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
public class SentinelPoolConfig {
public JedisSentinelPool createPool() {
Set<String> sentinels = new HashSet<>();
sentinels.add("192.168.1.10:26379");
sentinels.add("192.168.1.11:26379");
sentinels.add("192.168.1.12:26379");
// 连接池配置
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(20);
poolConfig.setMinIdle(5);
poolConfig.setMaxWaitMillis(3000);
poolConfig.setTestOnBorrow(true);
poolConfig.setTestWhileIdle(true);
return new JedisSentinelPool(
"mymaster", // 主节点名称
sentinels, // Sentinel 节点
poolConfig, // 连接池配置
2000, // 连接超时(ms)
2000, // 读取超时(ms)
"redis_password", // Redis 密码
0 // 数据库编号
);
}
}
5.2 Lettuce 连接 Sentinel
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
public class LettuceSentinelExample {
public static void main(String[] args) {
// 构建 Sentinel URI
RedisURI redisUri = RedisURI.builder()
.withSentinel("192.168.1.10", 26379)
.withSentinel("192.168.1.11", 26379)
.withSentinel("192.168.1.12", 26379)
.withSentinelMasterId("mymaster")
.withPassword("redis_password".toCharArray())
.build();
// 创建客户端
RedisClient client = RedisClient.create(redisUri);
// 获取连接
StatefulRedisConnection<String, String> connection = client.connect();
RedisCommands<String, String> commands = connection.sync();
// 使用
commands.set("key", "value");
String value = commands.get("key");
System.out.println("Value: " + value);
// 关闭
connection.close();
client.shutdown();
}
}
5.3 Spring Boot 配置
# application.yml
spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.1.10:26379
- 192.168.1.11:26379
- 192.168.1.12:26379
password: redis_password
timeout: 3000ms
lettuce:
pool:
max-active: 100
max-idle: 20
min-idle: 5
max-wait: 3000ms
@Configuration
public class RedisConfig {
@Bean
public LettuceConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("192.168.1.10", 26379)
.sentinel("192.168.1.11", 26379)
.sentinel("192.168.1.12", 26379);
sentinelConfig.setPassword("redis_password");
return new LettuceConnectionFactory(sentinelConfig);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
5.4 Python 连接 Sentinel
from redis.sentinel import Sentinel
# Sentinel 节点列表
sentinels = [
('192.168.1.10', 26379),
('192.168.1.11', 26379),
('192.168.1.12', 26379)
]
# 创建 Sentinel 连接
sentinel = Sentinel(
sentinels,
socket_timeout=0.5,
password='redis_password' # Sentinel 密码(如果有)
)
# 获取主节点连接
master = sentinel.master_for(
'mymaster',
socket_timeout=0.5,
password='redis_password' # Redis 密码
)
# 获取从节点连接(用于读取)
slave = sentinel.slave_for(
'mymaster',
socket_timeout=0.5,
password='redis_password'
)
# 写入(主节点)
master.set('key', 'value')
# 读取(从节点)
value = slave.get('key')
print(f"Value: {value}")
# 查看主节点信息
print(sentinel.discover_master('mymaster'))
# ('192.168.1.10', 6379)
# 查看从节点信息
print(sentinel.discover_slaves('mymaster'))
# [('192.168.1.11', 6379), ('192.168.1.12', 6379)]
六、生产环境最佳实践
6.1 部署建议
6.2 配置建议
# ===========================
# 生产环境 Sentinel 配置
# ===========================
# 绑定地址(生产环境绑定内网 IP)
bind 192.168.1.10
# 端口
port 26379
# 后台运行
daemonize yes
# 日志
logfile /var/log/redis/sentinel.log
loglevel notice
# 监控配置
sentinel monitor mymaster 192.168.1.10 6379 2
# 主节点密码
sentinel auth-pass mymaster redis_password
# 主观下线时间(根据网络情况调整)
# 太短可能误判,太长影响可用性
sentinel down-after-milliseconds mymaster 30000
# 故障转移超时(包含各阶段的超时)
sentinel failover-timeout mymaster 180000
# 同时同步的从节点数量
# 设为 1 可以保证其他从节点仍可服务
sentinel parallel-syncs mymaster 1
# 禁止脚本重配置(安全考虑)
sentinel deny-scripts-reconfig yes
# 通知脚本(故障时发送告警)
sentinel notification-script mymaster /opt/scripts/sentinel-notify.sh
# 故障转移后通知脚本
sentinel client-reconfig-script mymaster /opt/scripts/sentinel-reconfig.sh
6.3 告警脚本示例
#!/bin/bash
# /opt/scripts/sentinel-notify.sh
# 故障通知脚本
# 参数说明
# $1: 事件类型 (+sdown, +odown, +switch-master, etc.)
# $2: 事件描述
EVENT_TYPE=$1
EVENT_DESC=$2
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
# 记录日志
echo "[$TIMESTAMP] Sentinel Event: $EVENT_TYPE - $EVENT_DESC" >> /var/log/redis/sentinel-events.log
# 发送告警(示例:发送到企业微信)
send_alert() {
curl -s -X POST \
-H "Content-Type: application/json" \
-d "{
\"msgtype\": \"text\",
\"text\": {
\"content\": \"Redis Sentinel 告警\n时间: $TIMESTAMP\n事件: $EVENT_TYPE\n详情: $EVENT_DESC\"
}
}" \
"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY"
}
# 根据事件类型决定是否告警
case $EVENT_TYPE in
"+odown")
send_alert
;;
"+switch-master")
send_alert
;;
"+sdown")
# 主观下线可能是误判,记录但不告警
;;
*)
;;
esac
6.4 监控指标
# 监控脚本
#!/bin/bash
# Sentinel 状态检查
check_sentinel() {
local host=$1
local port=$2
# 检查 Sentinel 是否存活
if ! redis-cli -h $host -p $port PING > /dev/null 2>&1; then
echo "ERROR: Sentinel $host:$port is down"
return 1
fi
# 获取主节点信息
local master_info=$(redis-cli -h $host -p $port SENTINEL master mymaster 2>/dev/null)
# 检查主节点状态
local master_status=$(echo "$master_info" | grep -A1 "flags" | tail -1)
if [[ "$master_status" == *"s_down"* ]] || [[ "$master_status" == *"o_down"* ]]; then
echo "ERROR: Master is down according to Sentinel $host:$port"
return 1
fi
# 检查从节点数量
local num_slaves=$(echo "$master_info" | grep -A1 "num-slaves" | tail -1)
if [ "$num_slaves" -lt 1 ]; then
echo "WARNING: No slaves available"
fi
# 检查 Sentinel 数量
local num_sentinels=$(echo "$master_info" | grep -A1 "num-other-sentinels" | tail -1)
if [ "$num_sentinels" -lt 2 ]; then
echo "WARNING: Less than 3 sentinels available"
fi
echo "OK: Sentinel $host:$port is healthy"
return 0
}
# 检查所有 Sentinel
check_sentinel 192.168.1.10 26379
check_sentinel 192.168.1.11 26379
check_sentinel 192.168.1.12 26379
七、常见问题处理
7.1 脑裂问题
解决方案:
# 主节点配置
# 至少需要 1 个从节点,且延迟不超过 10 秒,否则拒绝写入
min-replicas-to-write 1
min-replicas-max-lag 10
# 这样在分区 A 中,老 Master 因为没有从节点而拒绝写入
7.2 故障转移不成功
# 检查 Sentinel 日志
tail -f /var/log/redis/sentinel.log
# 常见原因:
# 1. quorum 不够:检查存活的 Sentinel 数量
# 2. 从节点不可用:检查从节点状态
# 3. 密码配置错误:检查 masterauth 和 auth-pass
# 手动触发故障转移测试
redis-cli -p 26379 SENTINEL failover mymaster
# 检查故障转移状态
redis-cli -p 26379 SENTINEL master mymaster | grep -E "(name|ip|port|flags)"
7.3 从节点不自动加入
# 检查从节点状态
redis-cli -p 26379 SENTINEL slaves mymaster
# 可能原因:
# 1. 从节点配置的主节点地址不对
# 2. 密码配置错误
# 3. 从节点没有开启 replica-read-only
# 解决方案:
# 手动让从节点加入
redis-cli -h slave_ip REPLICAOF new_master_ip 6379
7.4 客户端连接问题
// 问题:故障转移后客户端仍连接旧主节点
// 原因:客户端缓存了旧的主节点地址
// 解决方案1:使用 Sentinel 连接池(自动发现新主节点)
JedisSentinelPool pool = new JedisSentinelPool(...);
// 解决方案2:监听 Sentinel 事件
public class SentinelListener extends JedisPubSub {
@Override
public void onMessage(String channel, String message) {
if (channel.equals("+switch-master")) {
// 解析新主节点地址
String[] parts = message.split(" ");
String newMasterIp = parts[3];
int newMasterPort = Integer.parseInt(parts[4]);
// 更新连接池配置
updateMasterAddress(newMasterIp, newMasterPort);
}
}
}
八、Sentinel vs Cluster
| 对比项 | Sentinel | Cluster |
|---|---|---|
| 数据分布 | 单主节点存储全量数据 | 数据分片存储 |
| 扩展方式 | 只能扩展读(从节点) | 可以水平扩展读写 |
| 容量限制 | 受单机内存限制 | 可以突破单机限制 |
| 故障转移 | 需要外部 Sentinel | 内置故障转移 |
| 客户端 | 需要 Sentinel 感知 | 需要 Cluster 感知 |
| 复杂度 | 相对简单 | 相对复杂 |
| 适用场景 | 数据量小、高可用 | 数据量大、高并发 |
九、总结
Sentinel 核心要点
- 至少 3 个 Sentinel 节点:确保选举成功
- quorum 设置合理:通常为 N/2 + 1
- 监控主观下线和客观下线:理解两者区别
- 自动故障转移:无需人工干预
配置建议
| 配置项 | 建议值 | 说明 |
|---|---|---|
| down-after-milliseconds | 30000 | 主观下线时间 |
| failover-timeout | 180000 | 故障转移超时 |
| parallel-syncs | 1 | 同时同步的从节点数 |
| min-replicas-to-write | 1 | 防止脑裂 |
| min-replicas-max-lag | 10 | 防止脑裂 |
最佳实践
- Sentinel 分布在不同机器/机架
- 使用 Sentinel 感知的客户端
- 配置告警脚本及时发现问题
- 定期测试故障转移
- 配置 min-replicas-* 防止脑裂