MySQL 主从延迟:从单线程回放到 LOGICAL_CLOCK 并行
一句话结论(30s)
主从延迟的根因是从库 SQL 线程单线程串行回放 binlog——主库能多线程并发写、从库只能排队,一个大事务就阻塞后续所有事务。解法是开启 MTS 多线程并行回放;因为单库架构下 DATABASE 模式会退化成单线程,所以必须用 LOGICAL_CLOCK 模式按事务提交时间戳判依赖、做事务级并行。
核心原理(2min)
LOGICAL_CLOCK 依赖 binlog 里每个事务的 last_committed 与 sequence_number 两个字段:若 Tj.last_committed >= Ti.sequence_number,说明 Ti 在 Tj 提交前已提交、二者无写冲突,可在从库并行回放,否则必须串行。而 DATABASE 模式按 Schema 分组并行,同一库内事务只能串行,单库多表场景会退化成单线程。
底层深入(5-10min)
问题
MySQL 读写分离后主库 CPU 降下来了,但用户反馈”发完评论刷新看不到自己发的内容”。排查发现主从延迟 P99 达到 2s+。
根因:单线程 binlog 回放
MySQL 默认主从复制架构是两个线程:
主库:多个工作线程并发写入 → binlog
↓
从库:IO 线程 → 接收 binlog 写入 Relay Log
SQL 线程 → 单线程串行回放 Relay Log
问题:主库可以多线程并发写入,从库只能单线程排队回放。一个大事务(如批量 UPDATE)阻塞后续所有事务,延迟突增。
💭 想一想:为什么主库多线程、从库却只能单线程?——因为早期从库只有一个 SQL 线程串行回放 relay log,天然没法像主库那样并发;主库并发提交的事务到从库只能排队,一旦某个大事务拖住,后面全堵。所以”延迟”的本质是”回放吞吐 < 主库写入吞吐”,MTS 就是给回放这一侧加并发。
方案:MTS(Multi-Threaded Slave)
MySQL 5.7 引入 MTS,将 SQL 线程扩展为多个 Worker 线程并行回放:
-- 传统模式(单线程)
slave_parallel_workers = 0
-- MTS 模式(4 线程并行)
slave_parallel_workers = 4
slave_parallel_type = LOGICAL_CLOCK
DATABASE 模式 vs LOGICAL_CLOCK 模式
| DATABASE | LOGICAL_CLOCK | |
|---|---|---|
| 并行粒度 | Schema 级别 | 事务级别 |
| 同一 DB 内并发 | ❌ 串行 | ✅ 并行 |
| 适用场景 | 多库架构 | 单库多表 |
DATABASE 模式的陷阱:如果整个应用只有一个数据库,所有事务都属于同一个 Schema,4 个 Worker 线程退化成”单线程”——这就是加了 slave_parallel_workers=4 但延迟没变化的原因。
💭 想一想:为什么”按 Schema 并行”在单库下会失效?——因为 DATABASE 模式的并行粒度是”库”:它保证同一库内事务串行、只允许不同库之间并行;如果整个应用只有一个库,所有事务都落进同一把锁、同一队列,Worker 再多也只能一次跑一个,等于白开线程。要单库也并行,必须把粒度细到”事务”级别,这正是 LOGICAL_CLOCK 登场的原因。
LOGICAL_CLOCK 的底层原理
MySQL 5.7.22+ 的 LOGICAL_CLOCK 模式基于事务提交时间戳判断依赖关系:
-- 每个事务在 binlog 中有两个关键字段
-- last_committed:事务提交前,最后一个已提交事务的序号
-- sequence_number:当前事务的序号
核心规则:任意两个事务 Ti 和 Tj,如果 Tj.last_committed >= Ti.sequence_number,说明 Ti 在 Tj 提交前已提交 → Ti 和 Tj 在从库可以并行回放。
主库并发提交时间线:
T1 ████████████
T2 ██████████
T3 ██████████
T4 ███████████
T1.last_committed=0, T1.seq=1
T2.last_committed=0, T2.seq=2 → T2 提交时 T1 未提交(0<1),不可与 T1 并行
T3.last_committed=2, T3.seq=3 → T3 提交时 T1/T2 已提交(2≥1,2≥2),可与 T1/T2 并行
T4.last_committed=2, T4.seq=4 → T4 提交时 T1/T2 已提交(2≥1,2≥2),可与 T1/T2 并行
💭 想一想:这个
last_committed >= seq判据凭什么能保证”并行回放不出错”?——因为它刻的是”提交顺序”:Tj 的 last_committed 是它提交前最后一个已提交事务的序号,如果 Tj.last_committed >= Ti.seq,说明 Ti 在 Tj 提交前就已提交完毕,二者没有并发写交集(无冲突),在从库怎么排都安全。反之说明 Ti 和 Tj 在时间上有重叠、可能有写冲突,必须按原顺序串行。
效果
SET GLOBAL slave_parallel_type = 'LOGICAL_CLOCK';
SET GLOBAL slave_parallel_workers = 4;
| 指标 | 单线程回放 | LOGICAL_CLOCK 4线程 |
|---|---|---|
| 主从延迟 P99 | 2s+ | < 50ms |
| 从库 CPU | 45% | 65% |
| 抖动频率 | 每天数十次 | 0 |
章末提问
追问 1:主从延迟的根因是什么?MTS 是怎么解决的?
回答思路:结论先行——根因是从库 SQL 线程单线程串行回放,回放吞吐跟不上主库并发写入;MTS 把 SQL 线程扩展成多个 Worker 并行回放。因为主库多线程并发写、binlog 是并发生成的,但从库默认一个 SQL 线程排队回放,大事务一堵后面全延迟;slave_parallel_workers + 并行类型让多事务同时回放,延迟大幅下降。
追问 2:为什么 DATABASE 模式在单库架构会退化成单线程?
回答思路:结论先行——因为 DATABASE 的并行粒度是 Schema,它只允许不同库之间并行、同一库内必须串行。因为单库架构下所有事务都属于同一个库,全部落进同一条串行队列,Worker 再多一次也只能跑一个,所以加了 slave_parallel_workers=4 延迟也不变。要单库并行,必须把粒度细到事务级,用 LOGICAL_CLOCK。
追问 3:LOGICAL_CLOCK 怎么判断两个事务能不能并行回放?
回答思路:结论先行——靠 binlog 里每个事务的 last_committed 与 sequence_number 两个字段:Tj.last_committed >= Ti.sequence_number 则可并行,否则串行。因为 last_committed 记录了该事务提交前最后一个已提交事务的序号,若 Ti 的序号不大于 Tj 的 last_committed,说明 Ti 在 Tj 提交前就已提交完毕、二者无并发写交集,回放顺序可以互换;否则有时间重叠、可能有冲突,必须按原序串行。