Redis主从+哨兵+Cluster:三种架构的选型与切换代价
2026/4/30大约 7 分钟
Redis主从+哨兵+Cluster:三种架构的选型与切换代价
适读人群:Java后端开发、架构师、对Redis高可用架构感兴趣的工程师 | 阅读时长:约24分钟
开篇故事
2021年,我们的Redis单机挂了。
凌晨3点,一台Redis服务器的磁盘满了(RDB快照写不下去),进程假死。20个服务实例的缓存全挂,请求全部打到MySQL,MySQL在1分钟内CPU打满,然后整个系统雪崩。
恢复用了40分钟:重启Redis(重启后要重新加载数据),MySQL在这40分钟内持续过载,部分请求超时。
这次事故之后,我们花了两周把Redis从单机迁到了哨兵架构,后来数据量增长又迁到了Cluster。每次迁移都踩了不少坑。
今天把三种Redis高可用架构的原理、选型和迁移代价完整讲一遍。
一、三种架构的核心区别
单机Redis:
→ 无高可用,任何故障即全面不可用
→ 适合:开发环境、数据量小且可接受短暂不可用的场景
主从复制(Master-Slave):
→ 读写分离,但主节点故障需要手动切换(无自动故障转移)
→ 适合:读多写少,且有DBA可以手动处理故障
哨兵(Sentinel):
→ 主从复制 + 自动故障转移(Sentinel集群监控Master,Master宕机自动选举新主)
→ 适合:中小规模,单Master即可满足写入性能的场景
→ 不解决:数据量超过单机内存的问题
Cluster(集群):
→ 数据分片(16384个slot,分布在多个主节点)+ 自动故障转移
→ 适合:数据量大(超过单机内存),或写入TPS超过单机上限
→ 代价:部分命令不支持(MGET跨slot、KEYS、SORT等)二、底层原理:三种架构的工作机制
2.1 主从复制原理
全量同步(初次建立主从关系):
Slave → Master: PSYNC replication_id offset
Master:
1. 执行BGSAVE生成RDB快照
2. 将生成快照期间的写命令记录到replication buffer
3. 发送RDB文件给Slave
4. 发送replication buffer中的增量命令
Slave:
1. 接收RDB,清空本地数据,加载RDB
2. 执行增量命令,与Master同步
增量同步(断线重连):
Slave重连后发送: PSYNC replication_id offset
Master检查:offset是否在repl_backlog_buffer中
→ 在buffer中:发送offset之后的增量命令(部分同步)
→ 不在buffer中:全量同步(buffer默认1MB,可能被覆盖)2.2 哨兵故障转移流程
Sentinel集群(至少3个Sentinel节点):
正常状态:
Sentinel1 ─┐
Sentinel2 ─┼─ 每秒PING Master/Slave
Sentinel3 ─┘
Master宕机后:
1. 某Sentinel发现Master无响应,标记为主观下线(SDOWN)
2. 向其他Sentinel发送 SENTINEL is-master-down-by-addr
3. 超过quorum(通常是N/2+1=2)个Sentinel确认:客观下线(ODOWN)
4. Sentinel之间投票选出Leader Sentinel负责故障转移
5. Leader选出新Master(选择数据最新的Slave)
6. 通知所有Slave复制新Master
7. 通知客户端新的Master地址(客户端需要支持Sentinel协议)
切换时间:通常30~60秒(可配置)
切换期间:所有写请求失败,读请求可能读到旧数据2.3 Cluster数据分片
Cluster使用哈希槽(Hash Slot)分片:
Total Slots: 16384
数据路由:
HASH_SLOT = CRC16(key) % 16384
key="user:123" → CRC16 = 12345 → 12345 % 16384 = 12345
→ 12345号slot在哪个节点:Node2(假设Node2负责8192~12287号slot)
→ 请求路由到Node2
典型的3主3从Cluster:
Node1 (Master): slot 0~5460
Node2 (Master): slot 5461~10922
Node3 (Master): slot 10923~16383
Node4 (Slave of Node1)
Node5 (Slave of Node2)
Node6 (Slave of Node3)三、完整解决方案与代码
3.1 哨兵架构的Java配置
// Spring Boot + Lettuce连接哨兵
@Configuration
public class RedisSentinelConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
// 哨兵配置
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster") // 主节点名称(与sentinel.conf中sentinel monitor一致)
.sentinel("sentinel-node1", 26379)
.sentinel("sentinel-node2", 26380)
.sentinel("sentinel-node3", 26381);
sentinelConfig.setPassword(RedisPassword.of("your-redis-password"));
// 连接池配置(关键:read-from配置)
LettucePoolingClientConfiguration clientConfig =
LettucePoolingClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED) // 优先读从节点
.poolConfig(GenericObjectPoolConfig.builder()
.maxTotal(20)
.maxIdle(10)
.minIdle(5)
.build())
.build();
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
}
}# application.yml(Spring Boot 2.x方式)
spring:
redis:
sentinel:
master: mymaster
nodes:
- sentinel-node1:26379
- sentinel-node2:26380
- sentinel-node3:26381
password: your-redis-password
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 53.2 Cluster架构的Java配置
@Configuration
public class RedisClusterConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(
Arrays.asList(
"redis-node1:6379",
"redis-node2:6379",
"redis-node3:6379",
"redis-node4:6379",
"redis-node5:6379",
"redis-node6:6379"
)
);
clusterConfig.setMaxRedirects(3); // 重定向最大次数
// Cluster模式的连接池配置
ClusterClientOptions options = ClusterClientOptions.builder()
.topologyRefreshOptions(
ClusterTopologyRefreshOptions.builder()
.enableAllAdaptiveRefreshTriggers() // 自动感知集群拓扑变化
.refreshPeriod(Duration.ofSeconds(60))
.build()
)
.build();
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.clientOptions(options)
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
return new LettuceConnectionFactory(clusterConfig, clientConfig);
}
}3.3 Cluster的限制与工作绕过方案
// 限制1:MGET/MSET在不同slot的key上会失败
redisTemplate.opsForValue().multiGet(Arrays.asList("key1", "key2", "key3"));
// 如果key1, key2, key3在不同slot → 报错 CROSSSLOT Keys
// 解决方案:使用Hash Tag让相关key路由到同一个slot
// {user}:123:profile → 使用{user}作为Hash Tag
// {user}:123:orders → 两个key都包含{user},路由到同一slot
// CRC16("{user}") % 16384 决定slot
String key1 = "{user:123}:profile";
String key2 = "{user:123}:orders";
// 这两个key的slot由{user:123}决定,一定在同一个slot
redisTemplate.opsForValue().multiGet(Arrays.asList(key1, key2)); // 正常!
// 限制2:Lua脚本中涉及多个key,这些key必须在同一个slot
String script = "return redis.call('get', KEYS[1])";
// 如果KEYS[1]和KEYS[2]在不同slot → 脚本执行失败
// 同样用Hash Tag解决
// 限制3:事务(MULTI/EXEC)中的key必须在同一个slot
// 解决:分布式锁等需要事务的场景,key使用Hash Tag四、踩坑实录
坑1:哨兵切换时,客户端没有感知到新Master
现象:哨兵成功将Slave提升为新Master,但Java应用还在连接旧Master
→ 写操作全部失败(旧Master已宕机)
→ 告警:Redis connection refused
原因:使用了Jedis直连模式,没有使用Sentinel客户端错误配置:
// 错误:直连Redis IP,哨兵切换后地址失效
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory("192.168.1.100", 6379); // 直连Master IP
}正确配置:
// 正确:连接Sentinel,由Sentinel告知当前Master地址
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master("mymaster")
.sentinel("sentinel1", 26379)
.sentinel("sentinel2", 26380)
.sentinel("sentinel3", 26381);
return new LettuceConnectionFactory(sentinelConfig);
// Lettuce会自动订阅Sentinel的+switch-master事件,主动感知Master切换
}坑2:从单机迁移到哨兵,应用层需要修改连接配置
迁移步骤(停服迁移,有停机时间):
1. 搭建哨兵集群(sentinel1, sentinel2, sentinel3)
2. 将原单机Redis作为新主节点
3. 添加两个从节点(全量同步)
4. 等待主从同步完成
5. 修改应用配置(从直连IP改为哨兵配置)
6. 重启应用
注意:步骤5和6之间有短暂停机(生产建议在低峰期操作)# 迁移前
spring:
redis:
host: 192.168.1.100
port: 6379
# 迁移后
spring:
redis:
sentinel:
master: mymaster
nodes: sentinel1:26379,sentinel2:26380,sentinel3:26381坑3:Cluster扩容时,slot迁移导致临时性能下降
场景:Cluster从3主扩展到4主(增加1台新主节点)
→ 需要将现有节点的部分slot迁移到新节点
→ 迁移过程中,被迁移slot上的key访问会有ASKING重定向
→ 客户端每次访问被迁移的key都需要额外一次网络请求
→ 访问延迟从1ms上升到3-5ms(翻倍)
报警:Redis操作P99延迟从2ms上升到8ms解决方案:
# 在业务低峰期进行slot迁移(凌晨)
redis-cli --cluster reshard \
redis-node1:6379 \ # 任意一个集群节点
--cluster-from all \ # 从所有节点均匀迁移
--cluster-to new-node4:6379 \ # 迁移到新节点
--cluster-slots 4096 \ # 迁移4096个slot(16384/4=4096,每主节点均分)
--cluster-yes # 非交互模式
# 迁移过程中监控延迟,如果延迟过高,暂停迁移
redis-cli --cluster check redis-node1:6379五、总结与延伸
三种架构的选型决策树:
数据量能放入单机内存(<50GB)?
是 → 需要自动故障转移?
是 → 哨兵架构(3哨兵节点 + 1主2从)
否 → 主从复制(读写分离即可)
否 → Cluster架构(根据总数据量确定主节点数)
数据量/单机内存 = 最少主节点数(建议3的倍数)迁移代价的量化对比:
| 迁移路径 | 停机时间 | 复杂度 | 应用改动 |
|---|---|---|---|
| 单机→主从 | <5分钟 | 低 | 无(可选读写分离) |
| 主从→哨兵 | <10分钟 | 中 | 修改连接配置 |
| 哨兵→Cluster | 1-2小时 | 高 | 代码检查(MGET等命令) |
一个容易忽视的问题:无论哪种架构,Redis宕机到恢复的这段时间,下游服务都要能优雅降级,不能因为Redis不可用就直接崩溃。应用层必须有容错处理和降级逻辑。
