第四篇讲过 redo log:只要日志落盘,进程崩了也不丢数据。但 MySQL 还有另一份日志——binlog,用来做主从复制和数据恢复,由 SQL 层维护,和 InnoDB 的 redo log 是完全独立的两套系统、两份文件。一次提交要把两份日志都写对,如果中途崩溃,两份日志不一致会怎样?这篇讲 InnoDB 和 binlog 之间的两阶段提交(2PC)——就是解决这个问题的机制。
第四篇的错误日志里其实已经出现过这套机制的名字——"XA crash recovery"——当时没展开,这篇把它讲清楚。
redo log 由 InnoDB 存储引擎层维护,binlog 由 MySQL Server 层维护——两个不同的子系统,各自决定"什么时候把自己的日志写盘"。
从库靠 binlog 复制,已经拿到了这笔修改并执行了;但主库自己重启后如果把这个事务回滚掉,主库和从库的数据就不一样了。
主库这边数据已经改了,但这笔修改永远不会同步到从库——同样是主从不一致,而且这次连"重放一遍"都补救不了,因为原始请求早就返回成功了。
InnoDB 先进入"prepare"状态(相当于说"我准备好了,但还没最终生效"),再写 binlog,最后才真正提交 InnoDB。崩溃恢复时,靠"binlog 里有没有这笔"来决定 prepare 状态的事务该提交还是回滚。
提交一个事务,用 mysqlbinlog 把对应的 binlog 内容原样倒出来看。
这条 Xid = 61 就是 binlog 一侧的提交标记——它和 InnoDB 那边这次事务的 XA 事务 ID 是配对的。崩溃恢复时到底该不该提交某个"看起来还没完成"的事务,就是靠这个 XID 在 binlog 里存不存在来判断的。
sql/binlog.cc · mysql-server @ mysql-9.7.1, L6936 /* If the binary log was not properly closed it means that the server may have crashed. In that case, we need to call binlog::Binlog_recovery::recover() to: a) collect logged XIDs; b) complete the 2PC of the pending XIDs; c) collect the last valid position. */
收集到的 XID 集合,直接被交给存储引擎去核对:
sql/binlog/recovery.cc · mysql-server @ mysql-9.7.1, L53 binlog::Binlog_recovery &binlog::Binlog_recovery::recover() { ... this->m_no_engine_recovery = ha_recover(&this->m_internal_xids, &xa_list); ... }
InnoDB 这一侧,对应注册的正是我们在第四篇错误日志里见过的那套"XA crash recovery"逻辑——两个真实存在的钩子函数:
storage/innobase/handler/ha_innodb.cc /** This function is used to prepare an X/Open XA distributed transaction. */ static int innobase_xa_prepare(handlerton *hton, THD *thd, bool all); /** This function is used to recover X/Open XA distributed transactions. @return number of prepared transactions stored in xid_list */ static int innobase_xa_recover(handlerton *hton, XA_recover_txn *txn_list, uint len, MEM_ROOT *mem_root); innobase_hton->recover = innobase_xa_recover;
拿到 InnoDB 里"处于 prepare 状态但还没提交"的事务列表,和 binlog 里收集到的 XID 集合一比对,规则只有两条。
这套判定要保证的不变式很直白:崩溃恢复之后,一个事务在主库上是"已提交"状态,当且仅当它在 binlog 里——不多不少。这样任何从 binlog 复制数据的从库,看到的世界最终都会和主库完全一致。
这篇没有像第四篇、第六篇那样做一次真实的 kill -9 崩溃实验。
不是不想做——是这次崩溃窗口太窄。redo log 那次实验能靠命令行控制时序(先 COMMIT 返回、再杀进程,或者让事务停在 SLEEP 里不提交),窗口是以秒计的,黑盒操作完全够用。但 InnoDB "prepare 完成"到"binlog 写完"这中间,通常只有微秒级的间隙,普通发行版的 mysqld 二进制没有暴露任何办法能精确让进程停在这两步之间——MySQL 自己的测试套件靠的是内部编译的 DBUG 同步点(DBUG_EXECUTE_IF),普通用户拿不到。所以这篇的验证方式换成了:真实源码 + 真实 binlog 结构 + 逻辑仿真验证,外加第四篇里已经真实观察到的 "XA crash recovery" 日志作为佐证——但没有一次精确命中这个窗口的真实崩溃实验。这个限制如实写在这里,不假装做到了。