我先把话说在前头:Redis在生产环境跑了小半年,我一度以为主从复制部署好就万事大吉。真正被教育是在某个凌晨,主节点物理机突然宕机,从库数据是新的,但所有客户端还死盯着旧主库的地址,最终整整一个多小时业务写不进去,后台告警刷屏。那次之后我才意识到,主从复制解决的是数据冗余,而不是高可用,真正要上生产,必须有一套能自动完成“发现故障、选新主、切流量”的机制,这就是哨兵模式该干的事。
这篇实战记录,我会从零带着大家搭一套完整的 Redis 哨兵集群:一主二从三哨兵,然后集成到 Spring Boot 里,把自动故障转移和读写分离都跑通。文章不空讲理论,所有配置、代码、坑点都是我在实际项目里摸过的,适合刚把 Redis 用进业务、又想解决高可用问题的后端同学参考。
1. 项目概述与架构设计思路
1.1 为什么单靠主从复制救不了线上
很多新手会有一个误区:主从复制配上之后,主库挂了,手动把某个从库提拔成主库不就行了吗?理论上确实可以,但“手动”两个字在生产环境是致命的。故障发生时间是凌晨,你被电话叫醒,登录服务器,查复制状态,挑一台数据最新的从库执行 replicaof no one,再改所有业务客户端的连接地址,光这一套流程走完,半小时算是快的。这半小时里,订单超时、缓存穿透、会话失效,什么幺蛾子都能出来。
主从复制的本质是异步复制。主库收到写命令后,先写内存和 AOF/RDB,再异步发给从库,所以从库天然存在数据延迟。但它最大的问题,是缺少一个“大脑”去监控所有节点状态,并且在一个节点挂掉之后,自动决策如何恢复集群对外服务。哨兵模式就是为了补上这一块:它专门负责盯着主库和从库的存活状态,一旦主库失联,哨兵们凑在一起投票,选出新的主库,然后把新主库的地址推送给客户端。
1.2 一主二从三哨兵的拓扑设计
这次项目我用的是一主二从三哨兵的经典部署。三个 Redis 数据节点分别是:一个主库负责写,两个从库负责读和热备;另外起三个哨兵进程,负责监控数据和执行故障转移。
为什么哨兵一定要起三个,而不是一个或两个?这里涉及一个很关键的概念:quorum(法定人数)。哨兵之间需要通过投票达成共识才能判定主库客观下线,进而触发切换。如果只有一个哨兵,它自己觉得主库挂了,就直接切换,万一只是网络抖动、哨兵和主库之间的链路闪断,就会造成误切,白白产生一次脑裂风险。三个哨兵搭配 quorum=2,意思是最少要有两个哨兵都认为主库不可用,才会真正发起故障转移,这样能过滤掉不少偶发误判。
三个数据节点也有讲究。两个从库不只是为了读写分离的读能力扩展,更重要的是故障转移时的备选池。哨兵选择新主库时,会优先考虑数据最完整、复制偏移量最大的从库,如果只有一个从库,它数据要是正好落后很多,切换后丢失的数据范围就更大。多一个从库,就多一分在切换时挑到“数据比较新”的节点的概率。
1.3 哨兵、Cluster 和客户端直连,到底怎么选
方案选型这块,我在项目里也纠结过一阵。最初想的方案是直接上 Redis Cluster,毕竟集群模式自带分片和高可用,听起来更“高级”。但仔细评估后放弃了,原因很现实:我们的缓存数据量远没到单机装不下的程度,核心诉求是高可用和读流量分担,Cluster 的数据分片机制反而会引入跨 slot 访问的复杂度,比如批量操作受限、key 分散后缓存命中率下降,这些都是要额外付出的成本。
| 对比项 | 哨兵模式 | Redis Cluster | 客户端直连主从 |
|---|---|---|---|
| 自动故障转移 | 支持,哨兵投票切换 | 支持,集群内部协调 | 不支持,需人工介入 |
| 数据分片 | 不支持,所有节点承载全量数据 | 支持,按 slot 分片 | 不支持 |
| 读写分离 | 可配置,从库可分担读 | 官方不推荐从库处理读 | 需自行封装 |
| 运维复杂度 | 低,哨兵+主从即可 | 高,节点多、槽位管理 | 低,但故障处理麻烦 |
| 适用场景 | 数据量可控、读多写少、要求高可用 | 海量数据、需水平扩展 | 测试环境、容忍停机 |
从表格能看出来,哨兵模式对大多数中大规模业务的缓存场景都够用,而且 Spring Boot 对哨兵模式的支持非常成熟,配置成本很低。Cluster 则更适合数据量已经到了单机无法承载、业务需要水平扩展的场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与 Redis 哨兵集群搭建
2.1 实例规划与基础配置清单
先说节点规划。我在实际环境里用了三台 Linux 服务器,每台部署一个 Redis 数据节点和一个哨兵节点。端口规划如下:
| 服务器 | Redis 数据端口 | 哨兵端口 | 角色 |
|---|---|---|---|
| 192.168.1.101 | 6379 | 26379 | 主库 |
| 192.168.1.102 | 6379 | 26379 | 从库1 |
| 192.168.1.103 | 6379 | 26379 | 从库2 |
同一台机器上同时跑 redis-server 和 redis-sentinel,要注意配置文件里的端口、日志路径、PID 文件路径都要区分开,避免互相覆盖。生产环境尤其不建议把三个哨兵部署在同一台物理机上,否则这台机器一挂,哨兵可能凑不齐 quorum,故障转移就转不动了。我这次是每台机器一个哨兵,正好满足这个条件。
基础配置上,我统一给 Redis 设置了访问密码。这里有个特别容易踩的坑:主库配了 requirepass,从库除了要配自己的 requirepass,还要配 masterauth,否则从库连不上主库去复制数据。哨兵连 Redis 也要用到密码,需要配置 sentinel auth-pass,三个地方的口令必须完全一致。
2.2 主从复制搭建要点
我在三台机器上把 Redis 都装好之后,开始配置主从关系。从库的配置很简单,在 redis.conf 里加上一行:
conf复制replicaof 192.168.1.101 6379
masterauth your-redis-password
有些老版本还是用 slaveof 命令,Redis 5.0 之后官方把 slave 相关术语都换成了 replica,配置项也跟着变了。主库不用配置任何和主从相关的项,你只需要在从库上指定主库地址,从库启动时就会自动向主库发起复制请求。
启动顺序要注意,务必要先把主库启动起来,确认它正常接收连接,再启动从库。如果从库先启动,它会在日志里反复重试连接主库,虽然最终主库起来后能自动恢复复制,但 log 里会出现大量连接报错,排查问题时会干扰视线。
从库全部启动后,可以用下面这个命令查看复制状态是否正常:
bash复制redis-cli -h 192.168.1.101 -p 6379 -a your-redis-password info replication
正常情况下,输出里的 connected_slaves 应该是 2,并且能看到两个从库的 IP 和端口。从库自己那台上执行 info replication,master_link_status 应该是 up,表示和主库的复制链路是健康的。
2.3 三个哨兵部署与配置
哨兵的配置是独立的一套,我单独创建了 sentinel.conf 文件。核心配置如下:
conf复制port 26379
daemonize yes
pidfile /var/run/redis-sentinel.pid
logfile /data/redis/sentinel.log
sentinel monitor mymaster 192.168.1.101 6379 2
sentinel auth-pass mymaster your-redis-password
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 30000
sentinel parallel-syncs mymaster 1
逐行解释一下。sentinel monitor mymaster 192.168.1.101 6379 2 这行最关键:mymaster 是给这个主从集群起的名字,后边跟的是主库地址和端口,最后一个 2 就是 quorum 值,即至少两个哨兵都认为主库挂了,才触发切换。
down-after-milliseconds 配置的是哨兵判断某个节点失联的阈值,我设了 5000 毫秒,意味着连续 5 秒内哨兵和主库的心跳都失败,哨兵就在本地把它标记为主观下线。这个值不能设得太小,否则网络一抖动就误判;也不能太大,否则故障转移响应太慢,业务方等不起。failover-timeout 是故障转移的超时时间,parallel-syncs 表示新主库产生后,允许同时有多少个从库向它发起复制同步,设 1 是为了避免多个从库同时全量复制把新主库打挂。
哨兵配置完,启动顺序是:先确保三台机器的 Redis 数据节点都正常,然后依次启动三台机器的哨兵。可以用 redis-server sentinel.conf --sentinel 启动,也可以用 redis-sentinel sentinel.conf。启动后验证一下集群状态:
bash复制redis-cli -h 192.168.1.101 -p 26379 -a your-redis-password sentinel masters
这个命令会列出哨兵眼中的所有主从集群信息,重点关注 num-slaves 是否为 2,flags 是否为 master。再用 sentinel replicas mymaster 可以看到两个从库的状态,flags 应该是 slave。
3. Spring Boot 集成与读写分离实现
3.1 引入依赖与连接配置
Spring Boot 侧我用的版本是 2.7.x。pom 里需要引入 Redis 的 starter 和连接池依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
Spring Boot 2.x 之后默认的 Redis 客户端是 Lettuce,而不是老项目里常见的 Jedis。这个选型对哨兵模式尤其有意义:Lettuce 基于 Netty,连接是异步和复用的,并且对主从拓扑变化的感知能力更强,主库切换后能自动重连到新主库,Jedis 则需要手动处理连接重建。
application.yml 里的核心配置如下:
yaml复制spring:
data:
redis:
password: your-redis-password
timeout: 3000ms
sentinel:
master: mymaster
nodes:
- 192.168.1.101:26379
- 192.168.1.102:26379
- 192.168.1.103:26379
lettuce:
pool:
max-active: 50
max-idle: 20
min-idle: 5
注意这里配置的不是 Redis 主库地址,而是三个哨兵的地址。Spring Boot 的哨兵模式原理是:客户端启动时先连上哨兵,向哨兵询问“mymaster 当前的主库是哪个”,然后建立到主库的连接。主库发生切换之后,客户端会重新从哨兵获取新主库地址,整个过程对业务代码透明。
3.2 读写分离的三种实现思路
读写分离的落地方案,我在项目里验证过三种,各有取舍。
第一种是 Lettuce 原生的 ReadFrom 机制。给 Lettuce 连接工厂配置一个 ReadFrom.REPLICA_PREFERRED,客户端会自动把读命令分发到从库,写命令仍然只发主库。这个方案最省事,几行配置就搞定,而且对业务代码完全无侵入。
第二种是基于 Spring 的 AbstractRoutingDataSource 思路,自己维护两个 RedisTemplate,一个绑定主库,一个绑定从库,再通过 AOP 或注解在方法层面做路由。这种方案的优点是控制粒度很细,可以在方法级别决定走主库还是从库,但需要在切面里处理事务、嵌套调用等问题,样板代码多。
第三种是干脆不用 Lettuce,退回 Jedis,然后在连接池层面手动封装。这个方案最麻烦,Jedis 对哨兵模式的拓扑感知弱,连接切换逻辑要自己写,我验证完就放弃了,完全不推荐。
综合下来,我最终选择了 Lettuce 原生 ReadFrom + 精细化命令兜底的组合方案。
3.3 读写分离核心配置和降级策略
开启 Lettuce 读写分离,只需要一个配置类:
java复制@Configuration
public class RedisReadWriteConfig {
@Bean
public LettuceClientConfigurationBuilderCustomizer lettuceClientConfigurationBuilderCustomizer() {
return builder -> builder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
}
ReadFrom 有几种策略,我特别说明一下:
MASTER:所有读操作都走主库,等于没开读写分离。MASTER_PREFERRED:优先主库,主库不可用时才读从库。REPLICA:强制只读从库,从库全都不可用时直接报错。REPLICA_PREFERRED:优先从库,从库全都不可用时回落主库。
我选了 REPLICA_PREFERRED,纯粹是从可用性角度考虑。Redis 的从库是异步复制,数据存在延迟,极端情况下延迟还会放大,如果强制只读从库,很可能读到很旧的数据,影响业务正确性。而 REPLICA_PREFERRED 在从库全部挂掉时会把读流量自动导回主库,至少保证服务不中断,只是主库压力大一点。
但这里有个隐患必须提醒:REPLICA_PREFERRED 不会考虑从库的复制延迟。也就是说,即使从库落后主库几十万条命令,它照样会响应读请求。对一致性要求高的读操作,建议在代码里强制走主库。我的做法是为这类操作单独注入一个 masterRedisTemplate,它的连接配置不设置 readFrom,天然走主库,然后在代码里手动区分:
java复制@Service
public class OrderCacheService {
private final RedisTemplate<String, Object> masterRedisTemplate;
private final RedisTemplate<String, Object> replicaRedisTemplate;
// 读操作,允许轻微延迟
public Object getCachedOrder(String orderId) {
return replicaRedisTemplate.opsForValue().get(orderId);
}
// 强一致读或写操作,强制走主库
public void updateCachedOrder(String orderId, Object data) {
masterRedisTemplate.opsForValue().set(orderId, data);
}
}
这里还要特别注意:事务命令(MULTI/EXEC)、Lua 脚本、部分阻塞命令在读写分离模式下有额外约束。事务里的所有命令必须落在同一个节点上,如果事务跨越了主从节点,Redis 会直接报错。Lua 脚本也同理,脚本执行期间要保证数据在同一节点。好在 Lettuce 的 ReadFrom 只影响读命令的选节点,事务和脚本相关命令要确保走主库,所以我在路由层面做了规则拦截:写命令、事务、脚本全部绑定主库,普通 GET/EXISTS/HGET 等读命令才允许走从库。
4. 故障转移验证与实战效果
4.1 主节点宕机演练实录
配置完成不代表真的高可用,我做了一次完整的故障演练,把主库直接干掉看效果。这里我用了 redis-cli -p 6379 shutdown nosave 模拟异常宕机。
故障发生后,我立刻去观察哨兵的日志,整个切换过程大致可以用这几个阶段概括。
第一阶段,哨兵发现主库心跳超时,将主库标记为主观下线(sdown)。日志里会出现类似 +sdown master mymaster 192.168.1.101 6379 的记录。这几个日志分别是哨兵判定节点失联时打的标记,真实日志里也用的是这几个关键字,排查问题的时候可以拿它们做关键字告警。
第二阶段,其他哨兵也陆续发现主库失联,当主观下线的哨兵数量达到 quorum(2个)时,主库被标记为客观下线(odown)。日志记录是 +odown master mymaster 192.168.1.101 6379 #quorum 2/2。
第三阶段,哨兵们选举出一个 leader 哨兵,由它负责执行故障转移操作,日志里会出现 +vote-for-leader ...。随后 leader 会根据配置挑选新主库:优先选择复制偏移量最大的从库,如果偏移量相同,再看从库优先级 slave-priority,默认配置下所有从库优先级均为 100。
第四阶段,被选中的从库执行了 replicaof no one,正式晋升为主库,其余从库重新指向新主库发起复制。日志里能看到 +switch-master mymaster 192.168.1.101 6379 192.168.1.102 6379,这一行非常重要,它直接告诉我们主库已经从 101 切换到了 102。
我实测下来,整套流程在默认配置下的耗时是:故障检测 5 秒左右,加上选举和切换动作,总共在 10 到 15 秒之间。这个时间段内,对主库的写入请求会报错,这是正常现象,任何故障转移方案都存在一个不可用窗口。
4.2 客户端如何感知新主库
故障转移完成之后,最让我担心的其实是客户端连接。当时我盯着应用日志,怕 Spring Boot 那边的 RedisTemplate 连接全部断掉,需要重启应用才能恢复。
实测结果很惊喜:Lettuce 客户端能自动感知拓扑变化。原因在于 Lettuce 的哨兵模式底层有拓扑刷新机制,它会周期性地向哨兵节点询问当前的 Master 地址,同时也会监听连接断开事件。当主库从 101 切到 102 后,应用下一次向 Redis 发起请求时,Lettuce 会选择一个新的底层连接,这个过程业务代码无感知,RedisTemplate 的注入和调用方式完全没变。
如果你用的是 Spring Boot 默认配置,拓扑地刷新周期通常是比较快的,实际故障演练中,应用在有新请求进入时几秒内就自动恢复了连接。这里建议不需要额外调小刷新间隔,默认值在可用性和性能之间已经平衡得不错,频繁刷新反而会增加哨兵节点的压力。
4.3 故障转移读写分离的数据一致性
读写分离模式下的数据一致性,是一个绕不开的话题。我在切换后马上做了一个验证:向新主库写入一条数据,然后立刻从从库读取,发现读不到。这其实是正常的,因为主从之间是异步复制,写入主库的数据要过一会儿才能同步到从库。
在故障转移的背景下,这种延迟还有更危险的一面。旧主库 101 在宕机前接收了一些写命令,有的已经同步给从库,有的还在复制队列里没来得及发出去。哨兵切换时选择了复制偏移量最大的从库 102,但仍有可能丢失 101 上还没来得及复制到从库的最后几条写命令。
针对这种情况,我的做法是分业务处理。像用户会话、热点商品库存这类数据,丢失一条影响范围有限,接受最终一致性;但像订单状态更新这种关键数据,不能全依赖 Redis,要同时落数据库,以数据库为准,Redis 缓存即使丢了也会在下次读取时回源重建。实际项目中,我还会在故障转移发生后,对比新旧主库的 info replication 里的复制偏移量,确认数据差异范围,再决定要不要做针对性的缓存预热。
5. 常见问题排查与生产落地建议
5.1 高频故障速查表
和哨兵模式打了这么久的交道,我把踩过的坑整理成一张速查表,新手照着排查能省掉大量时间。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 应用启动报 Could not find master | 配置的哨兵地址不是哨兵节点,而是主库地址;或者 sentinel monitor 配置的集群名和 Spring Boot 里的 master 名不一致 | 确认配置文件里连的是 26379 端口,且主从集群名和 sentinel.master 保持一致 |
| 从库日志出现 master_link_status:down | 从库没配 masterauth,或主库密码和 masterauth 不一致 | 统一 requirepass 和 masterauth |
| 故障转移完成后,应用写数据报 READONLY | 客户端还连在旧主库上,旧主库降级成了从库,从库默认只读 | Lettuce 会在一段时间内自动感知;长时间未恢复则检查网络和哨兵连通性 |
| 切换后数据丢失 | 异步复制导致旧主库未同步的写命令丢失 | 关键数据以数据库为准,Redis 只做缓存;故障后对比新旧主库偏移量 |
| sentinel.conf 内容被自动修改 | 哨兵运行时会在配置文件里动态记录状态和拓扑变化,属于正常现象 | 不要手动编辑运行中的哨兵配置,重启时会被覆盖,需改配置就停哨兵改完再启动 |
| 网络抖动导致频繁切换 | down-after-milliseconds 设置过小 | 调大阈值,结合监控看网络 jitter 情况 |
| 从库读到严重旧数据 | 复制延迟过大 | 监控 master_repl_offset 和 slave_repl_offset 的差值,设置阈值告警 |
5.2 参数调优与生产监控要点
哨兵的默认参数只是一个起点,实际生产环境要做针对性调优。down-after-milliseconds 我推荐在 5000 到 10000 毫秒之间,如果你的服务器和 Redis 网络链路不够稳定,就取大值,避免误切换;反过来,如果业务对中断容忍度低,且网络质量有保障,可以收小到 3000 毫秒左右。这个参数本质上是在“误判成本”和“故障恢复速度”之间做权衡。
failover-timeout 也不能一直用默认值。它包含了很多环节的超时总和,比如等待其他哨兵投票、等待从库切换等。建议设置成 down-after-milliseconds 的 3 到 6 倍,太短容易导致切换过程中断,太长则拖慢整体恢复。
监控是哨兵模式落地不可跳过的一环。我只监控三样东西:复制延迟、哨兵切换事件、客户端连接数。复制延迟通过比较主从节点的 master_repl_offset 和 slave_repl_offset 得到,差值超过阈值就要告警。哨兵切换事件用日志关键字 +switch-master 做匹配,一旦出现,说明发生了主从切换,运维人员要能第一时间收到通知。客户端连接数用来预判 Redis 是不是要成为瓶颈,连接数暴涨往往说明连接池配置不合理或者有连接泄漏。
另外我强烈建议,每季度至少做一次故障转移演练。我第一次做演练的时候,发现很多问题都不是配置本身的问题,而是关联环节的隐患:某个哨兵进程悄悄挂了没发现、某个从库复制链路跑了太久数据落后严重、应用连接池 max-active 太小导致切换恢复后瞬间被打满。这些故障平时不演练根本暴露不出来。演练完成后,把新旧主库的切换日志、应用客户端重连耗时、读写恢复时间点都记录下来,和上一次演练做对比,能明显感受到系统在一点点变稳。
最后再分享一个小的实操建议:生产环境的哨兵配置、主从配置、Spring Boot 连接配置,建议放到统一的配置管理里,不要分散写在各自的服务器上。我在项目里把这些配置都收口到了配置中心,后面调整 quorum、超时时间、读写策略,只需要改配置中心,不需要挨台机器登录去改,方便太多。
如果你现在的 Redis 还停留在单点,或者主从复制全靠手动切换,真心建议不要等到线上故障再来补课。按照文章里的步骤先搭一套哨兵集群,跑一次故障演练,你就能直观感受到“自动故障转移”这几个字在关键时刻的价值。
