本文基于 MySQL 8.4 官方文档编写,重点厘清一个 极易踩坑的版本差异:在 MySQL 8.4 中,
binlog_transaction_dependency_tracking和transaction_write_set_extraction两个参数 已经被移除,服务器默认即启用 WRITESET 行为,不再需要(也无法)手动"开启"。
网上绝大多数关于 writeset 并行复制的文章都还停留在 MySQL 8.0 时代,会教你这样配置:
-- MySQL 8.0 时代的写法(在 8.4 中已经失效!) SET GLOBAL binlog_transaction_dependency_tracking = 'WRITESET'; SET GLOBAL transaction_write_set_extraction = 'XXHASH64';
这两条在 MySQL 8.4 中会直接报错,因为这两个系统变量已经被移除了。
根据官方文档 《What Is New in MySQL 8.4 since MySQL 8.0》:
binlog_transaction_dependency_tracking:在 MySQL 8.0.35 和 8.2.0 中被标记为弃用(deprecated),在 MySQL 8.4.0 中被 移除。该变量及其功能已内建到服务器内部,不再提供替代变量。 从 MySQL 8.4 起,当使用多线程复制时,主库始终使用 writeset 来生成 binlog 中的依赖信息,其效果等同于在旧版本中把binlog_transaction_dependency_tracking设置为WRITESET。
同样,
transaction_write_set_extraction 也已移除,writeset 的哈希算法
固定为 XXHASH64,不再可配置。
所以结论是:
|
版本 |
需要手动配置吗 |
说明 |
|
MySQL 5.7 |
需要 |
默认
|
|
MySQL 8.0 |
需要 |
默认
|
|
MySQL 8.4 |
不需要 |
变量已移除,服务器 默认就使用 WRITESET 行为 |
这也是为什么你的问题"writeset 怎么开启"在 8.4 语境下的正确答案是:
不需要开启,它已经是默认行为。你唯一还能配置的相关参数,就只剩下
binlog_transaction_dependency_history_size 了。
在 binlog 中,每个事务都会被打上两个
逻辑时间戳(logical timestamp),它们写在事务开头那条 GTID 事件上。这两个值就是
sequence_number 和
last_committed。
sequence_number = 1;它本质就是一个单调递增的计数器,标识"这个事务是当前 binlog 里的第几个事务"。
last_committed 指的是
与当前事务发生冲突的、最近提交的那个事务的 sequence_number。它恒满足:
last_committed < sequence_number
它的语义是:
当前事务必须等待
sequence_number = last_committed 的那个事务执行完之后,才能被执行。
从库的 MTS(多线程复制)协调线程读的就是这两个值,判断哪些事务可以并行回放:
last_committed,说明它们彼此之间没有依赖关系(都不依赖对方),就可以被并行执行。last_committed 指向了事务 A 的
sequence_number,那么 B 必须排在 A 之后执行。用一个例子说明,假设主库 binlog 中有这样一串事务:
seq=1 last_committed=0 ──┐ seq=2 last_committed=1 │ → 这 4 个事务 last_committed 相同, seq=3 last_committed=1 │ 说明彼此无冲突,从库可以并行执行 seq=4 last_committed=1 ──┘ seq=5 last_committed=4 ──→ 依赖 seq=4,必须等 4 执行完
last_committed 的值越小(越靠前),意味着该事务可以越早、越并行地被回放;反之
last_committed 越接近
sequence_number,并行度越低。
直观理解:
last_committed本质上是一个"依赖屏障"——它之前的那些事务里,凡是没被它依赖的,它都可以跟它们并行;它真正必须等的,是那个"拦路"的冲突事务。
这是最容易混淆的一处,必须单独拎出来讲清楚:
binlog 里的
sequence_number 和 GTID 里的
uuid:序号 是两码事,只是碰巧都叫"sequence number"。
|
|
binlog 的
|
GTID 里的
|
|
全称/出处 |
事务在 binlog 中的逻辑时间戳,写在 GTID 事件头里 |
GTID 的后半部分,格式
|
|
作用域 |
每个 binlog 文件内从 1 递增, 换文件就归零 |
整个实例生命周期内单调递增,跨 binlog 文件持续 |
|
用途 |
配合
|
全局唯一标识一个事务,用于故障切换、主从一致性校验 |
|
是否重复 |
会(不同 binlog 文件里都有 seq=1 的事务) |
不会(
|
|
谁能修改 |
服务器内部计算,用户不可控 |
事务提交时服务器自动分配 |
用
mysqlbinlog 看,一个事务会同时出现这两样东西:
#230101 12:00:00 server id 1 end_log_pos 1000 #GTID [commit=...] SET @@SESSION.GTID_NEXT= 'a1b2c3d4-...-1234567890ab:17'/*!*/; # last_committed=3 sequence_number=18
a1b2c3d4-...:17 里的
17 是
GTID 的 transaction_id(这个实例上第 17 个事务),跟 binlog 文件名无关;sequence_number=18 是
逻辑时间戳,是"当前这个 binlog 文件里的第 18 个事务",可能前一个 binlog 文件末尾也是 seq=18。假设主库切了 binlog 文件:
mysql-bin.000001 ... GTID :100 → sequence_number=1(文件内第 1 个) mysql-bin.000002 ... GTID :101 → sequence_number=1(又是文件内第 1 个!)
GTID 的序号从 100 连续涨到 101(全局唯一),但 binlog 的
sequence_number 在换文件后
重新归零到 1。这正是官方文档强调的:sequence_number 在每个 binlog 文件中编号重新从 1 开始。
sequence_number(binlog 逻辑时间戳) → 只服务于并行复制的依赖判断,跟
last_committed 是一对;uuid:序号 → 事务的全球唯一身份证,用于复制拓扑的定位和一致性。两者都叫 sequence,但一个管"这个文件里排第几",一个管"整个服务器上排第几、而且永不重复"。
在 MySQL 8.4 中,依赖跟踪采用
WRITESET 算法(这是默认且唯一的行为)。要理解
last_committed 的计算,先理解"writeset(写集)"这个概念。
每个事务在提交时,会为它 修改过的每一行计算一组哈希值,构成这个事务的"写集"(write set)。官方文档对哈希的来源定义如下:
事务中的每一行会向该事务的写集添加一个或多个哈希, 每个唯一键(unique key)对应一个哈希。如果该行没有任何非空的唯一键,则使用 整行数据的哈希。写集同时包含删除的行和插入的行;对于更新的行, 旧行和新行都会被包含。
这些哈希由「库名 + 表名 + 唯一键/主键值」通过 XXHASH64 算法计算得出,用于唯一标识"这一行被改动了"。
两个事务 如果写集有交集(存在相同的哈希值),就判定为冲突,也就是它们修改了同一行,必须串行执行;写集完全不相交,则判定为独立,可以并行。
主库在内存中维护一张 writeset 历史映射表,记录"最近修改过某一行数据的是哪个事务"。表的结构大致是:
row_hash → sequence_number(最后一次修改该行的事务编号)
历史映射表的官方源码里
m_writeset_history 的类型定义是:
它是一个以 行哈希(uint64)为 key 的 map。map 的特性决定了: 同一个 key(同一行)只能有一个条目,不可能存在两条相同 key 的映射。
当一个事务提交时,主库按以下步骤计算它的
last_committed:
last_committed 设为一个"历史起点值",记为
m_writeset_history_start,即当前内存历史映射表中
最小的 sequence_number。<hash, 当前事务的 sequence_number> 插入历史表;last_committed = max(last_committed, 历史表中该 hash 对应的 sequence_number),并把该 hash 对应的 sequence_number
更新为当前事务的编号。last_committed(等价于两者中更保守的那个,保证不遗漏依赖)。核心逻辑一句话总结:
last_committed= 该事务写集里每一行,在历史表中"上一次被哪个事务改过"的那个 sequence_number 的最大值。
也就是说,它依赖的,就是"改过同一批行的、编号最大的那个历史事务"。
假设有 4 个事务依次提交,都改同一张
orders 表:
|
事务 |
修改的行(简化) |
sequence_number |
计算出的 last_committed |
|
T1 |
主键 id=1 |
1 |
0(历史为空,无依赖) |
|
T2 |
主键 id=2 |
2 |
0(id=2 从未被改过,与 T1 无冲突) |
|
T3 |
主键 id=1 |
3 |
1(id=1 上次被 T1 改过,依赖 T1) |
|
T4 |
主键 id=3 |
4 |
0(id=3 从未被改过) |
结果:T2、T4 与 T1 的
last_committed 都是 0,彼此无冲突,从库可以把它们和 T1 一起并行回放;而 T3 因为又改了 id=1,必须等 T1 完成。
writeset 的威力就在这里:哪怕主库并发很低、这些事务根本没有发生组提交,也能通过"行级冲突分析"创造并行机会。
binlog_transaction_dependency_history_size 是 MySQL 8.4 中
唯一保留的、可配置的 writeset 相关参数。
|
属性 |
值 |
|
作用范围 |
Global |
|
是否动态 |
是 |
|
默认值 |
|
|
最小值 |
|
|
最大值 |
|
|
单位 |
行哈希(row hash)的个数 |
官方定义:
该变量设置 内存中保留的行哈希数量的上限,这些哈希用于查找"最近修改过某一行的事务"。一旦达到这个数量,历史记录就会被 清除(purge)。
binlog_transaction_dependency_history_size管的那张内存历史表,它的 存活范围就是一个 binlog 文件的生命周期。每当 binlog 发生 轮转(rotate)、开启一个新的 binlog 文件时,这张历史表会 被清空重建。所以任何时候,表里存的全都是 当前这一个 binlog 文件里的
row_hash → sequence_number映射。而同一个 binlog 文件内,sequence_number是严格从 1 递增、 不重复的——因此映射表的 key(sequence_number)天然唯一,不存在"分属不同 binlog 的重复编号"混在一起的情况。
这个参数直接决定了主库那张"历史映射表"能装多少条目,进而影响
last_committed 的取值:
1. 影响历史表的存活窗口
历史表存的是
<row_hash → sequence_number> 映射。它保留的哈希越多,"改过同一行的事务"能被追溯得越久。一旦历史被清除,那些较早事务的依赖信息就丢了。
2. 触发"序列化点"(serialization point)
官方文档明确指出,writeset 算法存在 周期性的序列化点,其中一个触发条件就是:
如果自上一个序列化点以来,提交的事务累计产生的 唯一哈希数达到了
binlog_transaction_dependency_history_size,就会强制插入一个序列化点。
序列化点的含义是:
它之后的所有事务,都被视为与它之前的所有事务冲突。这会导致跨越序列化点的事务
last_committed 被"拉高",并行度瞬时下降。
3. 历史被清除后的依赖丢失
当历史表满了、旧条目被清除后,如果一个新事务修改的行,其"上一次修改事务"已经被清除出历史表,那么 writeset 算法
无法再检测到这个依赖。此时会退回到更保守的 COMMIT_ORDER 方式来计算
last_committed(取两者中较大值),确保正确性不受影响,但并行度会降低。
|
取值 |
效果 |
代价 |
|
调大(如 500000+) |
历史窗口更长,能更久地追溯到"上次改同一行的事务",并行度更高,序列化点更少 |
占用更多内存(每个哈希约几十字节量级) |
|
调小(如 1000) |
省内存 |
历史很快被清除,依赖频繁丢失 → 频繁退化到 COMMIT_ORDER,序列化点更密集,并行度下降 |
实践建议:
25000 通常足够;writeset 算法并非对所有事务都生效。官方文档列出的情况,会让主库 退回使用 COMMIT_ORDER 来计算依赖,也就是退化成"按组提交顺序"的保守模式:
这些场景的共同后果是:
相关事务的
last_committed 会被按 COMMIT_ORDER 方式计算(值偏大、偏保守),并行度下降。这也解释了为什么"单库大量无主键表"的业务,即使开了 8 个 worker,并行回放效果仍然很差——不是配置错,是业务数据模型不支持 writeset。
在 8.4 上,你不需要设置任何东西来启用 writeset。可以这样确认两个已移除的变量确实不存在:
-- 会报错:Unknown system variable SELECT @@binlog_transaction_dependency_tracking; SELECT @@transaction_write_set_extraction;
而唯一保留的参数可以直接查看和调整:
-- 查看当前历史大小(默认 25000) SELECT @@global.binlog_transaction_dependency_history_size; -- 如需调大,动态设置 SET GLOBAL binlog_transaction_dependency_history_size = 100000;
从库侧仍需配置 MTS 相关参数(这些在 8.4 中依然需要):
-- 从库:启用逻辑时钟并行 + 多 worker SET GLOBAL replica_parallel_type = 'LOGICAL_CLOCK'; SET GLOBAL replica_parallel_workers = 8; SET GLOBAL replica_preserve_commit_order = ON;
要验证主库确实在为事务打依赖标签,可以用
mysqlbinlog 解析 binlog:
mysqlbinlog --base64-output=DECODE-ROWS -v mysql-bin.000001
在 GTID 事件里能看到类似输出:
# at 1000 #GTID ... SET @@SESSION.GTID_NEXT= 'xxxxxxxx-...:1'/*!*/; # last_committed=0 sequence_number=1
观察不同事务的
last_committed 是否大量相同(相同即可并行)、
sequence_number 是否连续递增,即可判断 writeset 是否在正常工作。
|
问题 |
答案 |
|
writeset 怎么开启? |
MySQL 8.4 无需开启,服务器默认即 WRITESET 行为;
|
|
sequence_number 是什么? |
每个 binlog 文件中事务的全局递增编号,第一个事务为 1,换文件重新从 1 计( 注意:不是 GTID 里的 uuid:序号) |
|
last_committed 是什么? |
与当前事务冲突的、最近提交事务的 sequence_number,恒小于 sequence_number,代表"依赖屏障" |
|
主库怎么算 last_committed? |
遍历事务写集,在内存历史表中查找"上次改同一行的事务编号",取最大值;再与 COMMIT_ORDER 结果取保守值 |
|
history_size 有什么影响? |
决定历史表能保留多少个行哈希,影响依赖可追溯的窗口长度,并在达到阈值时强制插入序列化点、清空历史 |
核心记住三点即可:
last_committed 的语义是"我能和谁并行、必须等谁",值越小并行度越高;binlog_transaction_dependency_history_size 是你在 8.4 上唯一还能调的 writeset 相关参数,它管的是"主库能记住多远的行级依赖"。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/29654823/viewspace-3188656/ ,如需转载,请注明出处,否则将追究法律责任。