刚改完数据刷新就不见了?如果你第一次遇到这个问题,多半会怀疑缓存出 bug,其实大概率是主从延迟把“读后写”(Read Your Writes,读己之写一致性)给坑了。前一阵有同事拍桌子问我:“用户改完昵称,保存提示明明弹出来了,刷新一遍又变回旧昵称,主库上数据明明在,凭什么说人家没保存成功?”
这个问题我前后处理过好几轮,网上讲一致性的文章很多,但大多数要么停在概念层,要么只丢一个“关键读走主库”的结论就跑。这篇我想把整个链路摊开聊清楚:写入成功到底意味着什么,从库落后到底卡在哪几个环节,“读后写”陷阱在什么条件下必然出现,以及我实测过有效的四类处置方案。写这篇主要是给正在做读写分离、或者刚被这类幽灵 bug 折磨过的后端同学一个可落地的参考,读完你能带着排查思路回去。
1. 先复现现场:为什么“保存成功”和“刷新消失”能同时成立
1.1 “保存成功”只代表主库接受了
先看最典型的现场。一套常见的 Web 架构:主库负责写,若干从库分担读,应用层通过中间件或数据源路由做读写分离。用户提交修改资料请求,应用把 UPDATE 发到主库,主库返回 OK,接口告诉用户“保存成功”。紧接着用户刷新页面,浏览器发出一条新的查询请求,这条请求被路由到了一个从库上。
问题就出在这里:如果这个从库还没收到刚才那条 UPDATE 的 binlog,或者收到了 binlog 但还没来得及执行,它查出来的仍然是旧数据。用户看到的情况就是“刚改完,一刷新就没了”。
从系统的视角看,没有任何一个环节“出错”:
- 主库确实完成了提交,数据没问题;
- 从库确实只包含了它当前已执行到的事务,旧数据在它的快照里也是事实;
- 应用层只是把读请求均匀分发到了多个节点,恰好落到了一个“还没追上”的节点上。
所以这本质上不是某个组件坏了,而是异步复制架构下,数据存在多个副本,而副本之间有一个天然的时间窗口。这个窗口里,不同节点对外回答的是不同版本的事实。
1.2 为什么这次“消失”,上次却没有
很多团队第一次遇到这个问题时,会觉得它像个随机 bug:有时复现,有时不复现,用户一刷新可能又好了。因为它是否发生,取决于三个变量在某一瞬间是否同时成立:
- 读请求被路由到了从库(而不是主库);
- 从库当前存在延迟;
- 从“写入提交”到“发起读请求”的时间间隔,小于该从库追上该事务所需的时间。
三个条件缺一个都不会触发。这也是这类问题难排查的原因——正常情况下读写间隔可能只有几十毫秒,从库延迟往往也是几十毫秒到几秒之间波动,两者互相“赛跑”,时而追得上,时而追不上。
“刷新”这个动作其实加剧了问题。用户觉得“刷新一下是再看一眼”,但从系统的角度看,每次刷新都是一条全新的请求,负载均衡可能把它分给不同的从库。假设你有 3 个从库,其中一个正被大查询拖慢,用户第一次刷新命中它,看到旧数据;第二次刷新命中另外两个正常的从库,数据又“变”了。这种反复横跳,特别容易被用户理解为“系统出 bug 了”。
提示:排查这类问题时,先不要怀疑用户、不要怀疑缓存,第一步永远是确认“写入落的是哪个节点,读取打的是哪个节点”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从 binlog 到 SQL 线程:主从延迟到底卡在哪几个环节
2.1 一条写入在从库上要经过三程转运
要理解延迟为什么不可能彻底消灭,得先把复制链路看清楚。以 MySQL 为例,主从复制大致是这么跑的:
- 主库上事务提交,记录 binlog(二进制日志);
- 从库的 IO 线程通过网络把 binlog 拉到本地,写入 relay log(中继日志);
- 从库的 SQL 线程读取 relay log,逐条在从库上重放执行。
这中间每一步都可能产生等待。第 1 步取决于主库刷 binlog 的频率和磁盘能力;第 2 步取决于网络带宽和延迟;第 3 步取决于从库自身的执行能力,这是延迟最主要、最不可控的一环。
默认的 MySQL 复制是异步的:主库并不关心从库是否收到、是否执行完。即使开启半同步复制(semi-sync),也仅仅保证“至少有一个从库收到了 binlog”,而不是“所有从库都执行完成”。你连接的可能恰好是这个“已收到但还没执行完”甚至可以不是“收到”的那一个从库。
2.2 延迟的六个常见来源
很多 DBA 会把延迟归因为“网络慢”,但实际生产中,从库 SQL 线程的执行能力才是大头。我把这几年见过的主从延迟来源归纳成六类:
- 大事务:一条 UPDATE 影响几百万行,主库执行 30 秒,binlog 里就会积压大量事件,从库要继续执行 30 秒甚至更久。批处理任务、定期归档、全表更新是重灾区;
- 从库串行重放:早期 MySQL 从库 SQL 线程是单线程执行的,主库再快,从库也只能一条一条来。MySQL 5.7 之后有了并行复制,但受事务依赖和主库并行度影响,效果可能打折扣;
- DDL 操作:主库执行完一条 DDL 后,从库重放 DDL 时同样需要拿元数据锁,期间后续事务全部排队;
- 从库自身负载:很多团队会拿从库跑报表、做备份、承接复杂统计查询,这些操作会和 SQL 线程抢 CPU、内存、磁盘 IO;
- 网络抖动与 relay log 写入慢:IO 线程拉取跟不上主库产生 binlog 的速度,relay log 落盘慢,都会让链条吃紧;
- 锁等待和重试:从库重放事务时遇到行锁、间隙锁冲突,需要等待,累积起来也会形成明显延迟。
理解了这些来源,你就明白一个事实:主从延迟不是一个“调优后可以归零”的指标,它是一个随时会因业务流量、后台任务发生波动的不确定量。你只能把延迟控制在一定水平,但永远不可能保证“读的瞬间它恰好为零”。
2.3 怎么判断从库落后了多少
最直觉的方法是看从库状态:
sql复制SHOW SLAVE STATUS\G
关键字段有这些:
| 字段 | 含义 | 注意点 |
|---|---|---|
| Slave_IO_Running | IO 线程是否在拉 binlog | 必须是 Yes |
| Slave_SQL_Running | SQL 线程是否在重放 | 必须是 Yes |
| Seconds_Behind_Master | 从库落后主库秒数 | 最常用,但最容易被误解 |
| Read_Master_Log_Pos | 已拉取到主库 binlog 的位置 | 说明“收到哪了” |
| Exec_Master_Log_Pos | 已执行到主库 binlog 的位置 | 说明“应用到哪了” |
`Secon
