读写分离下「写主库、读从库」最常见的一致性坑,一文讲清成因、判断、预防与兜底
很多系统为了扛住读流量,会做 读写分离:写请求走 主库(Master),读请求走 从库(Slave)。 但主从之间是 异步复制 的——主库写完,从库不一定立刻跟上。于是一个经典 bug 就出现了: 刚改完数据,页面一刷新,发现改的东西「没生效」。这背后就是「主从延迟导致读到旧数据」。
MySQL 主从复制是一条流水线,数据要从主库「流」到从库并「重放」完,中间任何一环慢了,就会产生延迟(单位通常是秒,记为 Seconds_Behind_Master)。
slave_parallel_workers)缓解。问题几乎总是同一个模式:「写主库」和「读从库」发生在极短的时间间隔内,而从库还没追上。 下面这条时序线是最典型的翻车现场。
先说一个残酷的真相:从库自己并不知道「业务上」它落后了多少。
Seconds_Behind_Master 只能告诉你从库整体落后几秒,无法告诉你某一条具体记录是不是旧的。
所以「判断读到的是不是旧数据」通常要从两个层面入手:
Seconds_Behind_Master 判断「此刻从库是否值得信任」Seconds_Behind_Master = 0 也不绝对安全。它只反映 relay log 回放 的延迟,不反映 IO 线程(网络接收)的延迟;
而且当主库长时间无写入时,该值会固定在 0,即使复制链路其实有网络中断也显示 0。更可靠的做法是结合 GTID 和复制心跳监控。
最实用的业务级判断法是让写操作返回一个「凭据」,读回来后比对。常见三种凭据:
updated_at 或 version;前端/接口拿着这个值去读从库,若读回的 version 小于期望值,说明读到旧数据。WAIT_FOR_EXECUTED_GTID_SET / 查 @@gtid_executed 判断从库是否已应用该事务。PAID,读回来若是 UNPAID 直接判定为旧/不一致。Seconds_Behind_Master + GTID 复制状态,超阈值就别读从库。按「彻底程度」从高到低,下面是实战中最常用的方案。绝大多数系统不是只选一个,而是组合使用。
把所有「写后立即读」的请求,以及核心一致性要求高的读(余额、订单状态),直接路由到主库;只有锦上添花的统计/列表类读走从库。 优点:零延迟问题,实现最简单。缺点:主库压力大,读写分离收益打折。适合读流量没那么恐怖的系统。
识别「同一用户/会话的写后读」模式,在写完后的一个短窗口内,该用户的相关读请求强制路由到主库,窗口过后恢复读从库。常见实现:
/* FORCE_MASTER */ 标记,中间件(如 ShardingSphere、MyCat)据此路由。利用 GTID 做「读前等待」:主库写入后拿到 GTID,读之前让从库执行
SELECT WAIT_FOR_EXECUTED_GTID_SET('<刚写入的GTID>', <超时秒>),
等从库真正回放到该事务后再读。MySQL 8.0 还能在连接上开启 session_track_gtids 自动跟踪。
优点:严格保证读到新值,又不浪费主库读。 缺点:每次多一次等待 RT,实现偏复杂。
开启 rpl_semi_sync_master_enabled,主库提交事务时要等至少一个从库收到 binlog 才返回成功(after_sync 模式)。
这能降低数据丢失风险,但注意:从库「收到」≠「回放完」,读从库仍可能落在回放之前,不能完全杜绝旧数据。把它当作「减少延迟幅度」的手段,而不是银弹。
写主库的同时,把最新值写入 Redis(带短过期)。读请求先查从库,若发现可能不一致(或缓存命中)就用缓存里的新鲜值。 优点:读从库依然扛量,又避开了回放延迟。注意:要处理好「缓存与数据库一致性」(如先删缓存再写库、或延时双删),否则会引入新的坑。
持续监控 Seconds_Behind_Master 和 GTID 差距,一旦从库延迟超阈值,自动把所有读切回主库(或拒绝走从库),延迟恢复后再切回。这是防止「从库大落后时集体读旧」的保险丝。
| 方案 | 一致性保证 | 主库压力 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| A 读写都走主库 | 强 | 高 | 低 | 读量不大 / 强一致核心 |
| B 写后读走主库 | 强(关键读) | 中 | 中 | 最常用 |
| C 等 GTID 同步 | 强 | 低 | 高 | 严格要求无旧数据 |
| D 半同步复制 | 弱(降概率) | 低 | 中 | 防数据丢失为主 |
| E 缓存兜底 | 中 | 低 | 中 | 读量大且容忍最终一致 |
| F 监控自动降级 | 兜底 | 动态 | 中 | 所有读写分离系统必备 |
就算方案做足,极端情况下仍可能偶尔读到旧值。这时候要有一套兜底处理流程,把影响压到最低。
Seconds_Behind_Master / GTID 知「该不该读从库」;业务层用 version / 时间戳 / 期望值知「这条是不是旧」。Seconds_Behind_Master 监控自动降级,基本就能兼顾性能与一致性。