Redo Log 和 Binlog 为什么需要两阶段提交?

问题出在两份日志

redo log 和 binlog 是两份独立的日志,由不同的模块维护,写的时候是两个独立的动作。事务提交的时候两份都得写,但没有谁保证这两次写能一起成功。

中间任何一步出错,就会出现"一份有一份没有"的错位。而这两份日志各自还担着不同的责任:

  • redo log 决定崩溃恢复之后主库上有没有这个事务
  • binlog 决定从库 和备份里有没有这个事务

所以只要两份对不上,主库和从库就必然对不上。下面看两种错位分别会怎样。

两种错位

redo 写了,binlog 没写

事务提交的时候先把 redo log 写了(刷盘),然后进程被 kill,binlog 还没来得及写。

崩溃恢复之后,redo log 里有这个事务,数据被重放出来了,主库上这个事务是生效的 。但从库那边从来没收到过这个事务的 binlog,从库上没有这份数据。

结果就是主库有的数据从库没有,从库少数据。

binlog 写了,redo 没写

反过来,binlog 先写成功了,redo log 还没刷盘就崩了。

从库能拉到这份 binlog,把这个事务执行一遍,从库上有这份数据 。但主库崩溃恢复的时候 redo log 里没有这个事务,主库上它不存在。

结果是从库多出了主库没有的数据。

小结

顺序 主库 从库 结果
先 redo 后 binlog,binlog 失败 有 没有 从库丢数据
先 binlog 后 redo,redo 失败 没有 有 从库多数据

两种都不行。不管把谁放在前面,另一份失败就会不一致,所以问题的本质不是"顺序"能解决的,需要额外一个机制把两份日志绑在一起,让它们要么都算数要么都不算数。

两阶段提交

MySQL 的做法是给 redo log 引入一个中间状态 prepare,把一次提交拆成两段:

text 复制代码
prepare 阶段
  1. 写 redo log,事务状态标记为 prepare
     此时 redo log 是完整的,但还不是 commit

commit 阶段
  2. 写 binlog
  3. 写 redo log,事务状态从 prepare 改成 commit

关键在于 binlog 夹在 redo 的 prepare 和 commit 中间。这个位置不是随便放的,它决定了崩溃恢复时的判断规则:

崩溃恢复的时候,如果一个事务在 redo log 里处于 prepare 状态,就去 binlog 里找它对应的完整记录。找到了就提交(把 redo 改成 commit),没找到或者不完整就回滚。

判断依据是 binlog 里那个事务的 Xid 事件,能和 redo log 里的 XID 对上,就说明 binlog 完整写完了。

崩溃发生在两个时刻

用这个规则回头验证前面那两种错位,会发现在两阶段提交下都变成一致的。

时刻 A:写完 redo prepare 就崩

此时 redo log 里有一条 prepare 记录,binlog 里什么都没有。

恢复的时候发现这个事务处于 prepare 状态,去 binlog 里找它的 XID,找不到,判定 binlog 没写完整,回滚。

主库没有这个数据,从库也没有(binlog 都没发出去),两边一致。

时刻 B:写完 binlog 就崩

此时 redo log 里有一条 prepare 记录,binlog 已经写完整了。

恢复的时候同样去 binlog 里找 XID,找到了 ,判定 binlog 完整,提交 ,把 redo log 里的状态改成 commit。

主库有这份数据,binlog 也已经写出去了从库能拉到,两边一致。

这里最反直觉的地方

时刻 B 的处理是整套机制的精华:redo log 里明明写的是 prepare,恢复时却按提交处理。

为什么敢这么决定?因为 binlog 已经完整了,而 binlog 是要发给从库的。如果这时候回滚,主库没有而从库有,又回到不一致。所以只要 binlog 完整,就必须提交。

换个角度看,两阶段提交实际上是把"binlog 是否完整 "当成了整个事务的提交标志,redo log 里的 prepare / commit 只是用来标记"这个事务还在不在判定中"。

时刻 B 崩溃有个副作用:客户端很可能收到的是超时或者连接断开,而不是"提交成功"。但数据实际是提交了的。所以业务侧还是要靠幂等来处理"超时重试导致重复提交"的问题。

为什么不能只用 redo log 的 commit 标记

可能会想:既然要判断,为什么不干脆先写 binlog、再写 redo 的 commit,一步到位?

因为那样会退化成"redo 的 commit 标记和 binlog 谁先谁后"的问题,绕回来了。两阶段提交的价值在于多出来的那个 prepare 状态本身就是一个可查询的标记,它让恢复时能问一句"这个事务的 binlog 写完了吗",而只靠 commit 标记是没有这个信息可查的。

这个名字容易和 XA 混

MySQL 说的"两阶段提交"和分布式事务里的 2PC(XA)不是一回事。

MySQL 的两阶段提交 XA 的两阶段提交
协调的是什么 同一个实例里的两份日志 多个独立的数据库或者资源管理器
有没有协调者 没有 有,事务协调者负责收集投票
有没有投票环节 没有 有,所有参与者都同意才提交
解决的问题 主从数据不一致 跨库事务的原子性

MySQL 支持的真正 XA 是另一套语法(XA START / XA END / XA PREPARE / XA COMMIT),用在跨多个库的场景。这里讲的两阶段提交只是借用了这个词,本质是"用一个 prepare 状态把两份日志绑在一起"。

MySQL 5.7 及之前有个 innodb_support_xa 参数可以关掉这套机制,8.0 直接把这个参数移除了,因为两阶段提交是保证主从一致的必要条件,没有关掉的道理。

组提交:代价和优化

两阶段提交的代价很直接:每个事务要写两次日志,redo 在 prepare 阶段一次、commit 阶段一次,binlog 一次,多了不止一次 fsync。

InnoDB 用组提交(group commit)把它摊薄:多个事务在同一轮里排队,由其中一个当 leader 负责真正的 fsync,其余跟着等结果,这样 N 个事务的 fsync 能被合并成一次。

在双 1 配置(sync_binlog = 1 + innodb_flush_log_at_trx_commit = 1)下,如果有两个参数可以把攒批的窗口开大一点:

  • binlog_group_commit_sync_delay:故意等一段时间再 fsync,等的时间里能攒下更多事务
  • binlog_group_commit_sync_no_delay_count:攒够多少个事务就立刻 fsync,不等了

两个参数是用来在"吞吐"和"延迟"之间调整的,设大了吞吐高但单条事务的响应变慢。默认都是 0,也就是不额外等待。

相关推荐
xyLJ1 小时前
Redis 常见的数据类型及底层结构
后端
我的div丢了肿么办1 小时前
go语言中的时间time
后端·go
136096757231 小时前
同一套 DeepSeek,三种框架六个坑
后端
哈尔ai1 小时前
事务边界设计实战:从单库原子性到跨服务可靠协作
后端
IT_陈寒1 小时前
Vue这个响应式陷阱我竟然踩了3次
前端·人工智能·后端
IT_陈寒1 小时前
Redis雪崩把我坑惨了,三招教你躲过去
前端·人工智能·后端
ikoala1 小时前
同样叫 Harness,DeepSeek Harness 和 Pi 根本不在同一层
前端·后端·ai编程
大白801 小时前
多模态大模型能干什么?图文音视频统一理解,正在重画 AI 的边界
后端
花卷持续成长1 小时前
“码上面试”项目学习02
后端