Skip to content
Go back

MySQL并行回放MTS

MySQL 主从延迟:从单线程回放到 LOGICAL_CLOCK 并行

一句话结论(30s)

主从延迟的根因是从库 SQL 线程单线程串行回放 binlog——主库能多线程并发写、从库只能排队,一个大事务就阻塞后续所有事务。解法是开启 MTS 多线程并行回放;因为单库架构下 DATABASE 模式会退化成单线程,所以必须用 LOGICAL_CLOCK 模式按事务提交时间戳判依赖、做事务级并行。

核心原理(2min)

LOGICAL_CLOCK 依赖 binlog 里每个事务的 last_committedsequence_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 模式

DATABASELOGICAL_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线程
主从延迟 P992s+< 50ms
从库 CPU45%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_committedsequence_number 两个字段:Tj.last_committed >= Ti.sequence_number 则可并行,否则串行。因为 last_committed 记录了该事务提交前最后一个已提交事务的序号,若 Ti 的序号不大于 Tj 的 last_committed,说明 Ti 在 Tj 提交前就已提交完毕、二者无并发写交集,回放顺序可以互换;否则有时间重叠、可能有冲突,必须按原序串行。


Share this post on:

Previous Post
MySQL 索引下推(ICP):让二级索引在引擎层就完成过滤
Next Post
MySQL 存储引擎架构五层模型:表空间→段→区→页→行