发布时间:2026-09-23 12:31 更新时间:2026-09-23 12:31 阅读量:0
做运维的朋友大概率遇到过这个场景:半夜 Redis 主节点故障,哨兵把从库提升为新主,服务看起来恢复了,但第二天业务侧开始报错,日志里清一色是 READONLY You can't write against a read only replica。这个报错字面意思是「你正在往一个只读副本写数据」,很多人第一反应是去查主从状态,其实大多数情况下主从关系是正常的,问题出在应用端连的还是那台已经被降级为从库的旧主。下面把三种拓扑下的成因和排查路径理清楚。
这个错误来自 Redis 自身的服务端校验,不是网络层或代理层返回的。当一个实例的角色是 replica(从库),并且配置项 replica-read-only 保持默认值 yes 时,任何来自普通客户端的写命令都会被直接拒绝,返回上述错误。注意它拒绝的是写命令,读命令照常执行,所以现象往往是「读接口正常、写接口全挂」,很容易被误判成业务代码问题。
先把当前实例的角色和主从关系确认一遍,不要凭印象判断:
redis-cli -h 10.0.0.11 -p 6379 INFO replication
关注这几行:
role:master 或 role:slave
master_host / master_port / master_link_status
若 role:slave 且 master_link_status:up,说明它是正常从库,写入必然被拒
redis-cli -h 10.0.0.11 -p 6379 CONFIG GET replica-read-only
默认返回 yes,改成 no 可以允许从库写入,但会破坏主从一致性,生产环境不建议
需要区分两种情形:一是应用确实连到了从库,属于配置问题;二是实例被误配成了从库,比如运维把一台本该独立的实例加入了复制关系。前者改连接配置即可,后者要先确认业务上是否真的需要这个复制关系。还有一种少见情况是 replica-read-only 被显式改成了 no,此时从库能写但数据不会同步回主,长期会形成数据分叉,属于隐患而非解决方案。
三种拓扑对客户端的要求完全不同,误配基本都出在这里。
纯主从模式最简单,应用直连某个 IP 和端口。它的致命问题是主节点地址写死在配置里,主挂了从升主,应用不知道新主是谁,于是继续往旧主写,而旧主此时已经变成从库,READONLY 就来了。这种模式没有自动发现能力,切换后必须人工改配置重启应用,只适合小规模或非关键场景。
哨兵模式在客户端侧引入了哨兵地址列表,由客户端通过哨兵查询当前主节点。关键点是:应用配置里应该填 sentinel 的地址和 master 名称,而不是 Redis 实例的地址。如果代码里用的是普通 Jedis 连接池直连某个 Redis IP,那哨兵切换对它毫无意义,报错几乎必然发生。以 Java 生态为例,正确的配置形态大致是这样:
# Spring Boot 中的哨兵配置示例(关键是 nodes 填哨兵端口,不是 Redis 端口)
spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=10.0.0.21:26379,10.0.0.22:26379,10.0.0.23:26379
spring.redis.password=your_password
哨兵端口默认 26379,Redis 端口默认 6379,两者不要写混
同时要确认哨兵自己认识的主节点名称和实际部署一致:
redis-cli -h 10.0.0.21 -p 26379 SENTINEL masters
输出里 name 字段就是 master 名称,必须与客户端配置的 mymaster 一致
redis-cli -h 10.0.0.21 -p 26379 SENTINEL get-master-addr-by-name mymaster
返回当前主节点的 IP 和端口,切换后可再执行一次对比是否变化
Cluster 模式没有哨兵,客户端通过 CLUSTER SLOTS 或 CLUSTER SHARDS 获取槽位与节点映射,并且必须使用支持集群协议的客户端。常见误配有两种:一是用了单机客户端连集群节点,节点会返回 MOVED 重定向,客户端不处理就报错,而 READONLY 多出现在把请求发到了从节点上;二是客户端缓存了槽位映射但没有刷新机制。集群里如果确实需要从节点提供读服务,客户端要显式发送 READONLY 命令开启只读模式,这一点和主从模式的 replica-read-only 是两回事,容易混淆。
排查时可以直接看节点视角:
redis-cli -h 10.0.0.31 -p 6379 CLUSTER INFO
cluster_state:ok 表示集群健康
redis-cli -h 10.0.0.31 -p 6379 CLUSTER NODES
看每个节点的角色(master/slave)与负责的槽位范围
redis-cli -h 10.0.0.31 -p 6379 CLUSTER SLOTS
客户端拿到的槽位映射以此为准,切换后映射会变化
切换之后能不能自动恢复,取决于客户端的拓扑刷新能力。连接池一般有两个相关参数:一个决定多久重新拉取一次拓扑,一个决定遇到连接异常或 MOVED 时是否立即刷新。生产环境建议把刷新周期设得短一些(比如几十秒量级),并开启异常触发刷新,这样主从切换后应用能在可接受的时间内自愈,具体参数名各客户端不同,以官方文档为准。如果业务不能容忍这段时间的写入失败,就需要在应用层加重试和降级逻辑,而不是指望 Redis 自己解决。
再说读写分离。从库读能分摊主库压力,代价是复制延迟带来的数据不一致,刚写入的数据立刻从从库读可能读不到。常见做法是:强一致的读走主库,报表、列表这类容忍延迟的读走从库。但要注意,一旦开启从库读,就必须保证连接池不会把写请求也路由到从库,否则又会撞上 READONLY。另外不建议为了让从库能写而关闭 replica-read-only,那等于放弃了主从一致性这个前提。至于 Cluster 下的从库读,需要客户端支持并显式开启只读模式,且要接受同样的延迟问题。
小结一下排查顺序:先确认报错实例当前的 role,判断是连错节点还是角色配错;再检查客户端用的是直连、哨兵还是集群客户端,配置里填的地址类型是否匹配;最后看拓扑刷新参数是否生效。切换后一段时间内仍报 READONLY,多半是客户端没有重新拉取拓扑,而不是 Redis 没切成功。建议把 SENTINEL get-master-addr-by-name 或 CLUSTER SLOTS 这类命令纳入监控巡检,切换发生时能第一时间确认主节点归属,减少排查时间。
| 📑 | 📅 |
|---|---|
| PostgreSQL表膨胀与autovacuum不生效排查实战 | 2026-09-23 |
| rsyslog 与 journald 双写日志重复或丢失排查 | 2026-09-23 |
| Linux conntrack 表满导致丢包:现场定位与容量评估 | 2026-09-23 |
| Nginx客户端与后端长连接谁在复用:端口耗尽排查顺序 | 2026-09-23 |
| Linux 服务器 CPU 软中断 si 偏高排查:网卡多队列、RPS 与内核参数调整 | 2026-09-23 |
| Linux负载高但CPU空闲:D状态进程与不可中断睡眠排查 | 2026-09-24 |
| Nginx 与 CDN 回源 IP 不一致:XFF 取值顺序与日志字段 | 2026-09-24 |
| Docker 容器内 Nginx 与宿主 Nginx 端口冲突排查 | 2026-09-24 |
| Nginx 443 端口 SNI 分流:WebSocket 与普通请求共用配置 | 2026-09-24 |
| MySQL 8 大事务致 undo 膨胀与主从延迟:定位与拆分提交 | 2026-09-24 |