二阶段提交的 prepare 阶段,binlog 和 InnoDB 各自会有哪些动作?
本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。
1. 二阶段提交
二阶段提交,顾名思义,蕴含两个阶段,它们是:
- prepare 阶段。
- commit 阶段。
咱们只思考 SQL 语句操作 InnoDB 表的场景,对于用户事务,是否应用二阶段提交,取决于是否开启了 binlog。
因为 MySQL 把 binlog 也看作一个存储引擎,开启 binlog,SQL 语句扭转(插入、更新、删除)InnoDB 表的数据,这个 SQL 语句执行过程中,就波及到两个存储引擎。
应用二阶段提交,就是为了保障两个存储引擎的数据一致性。
用户事务提交分为两种场景,如果开启了 binlog,它们都会应用二阶段提交。
场景 1:通过 BEGIN 或其它开始事务的语句,显式开始一个事务,用户手动执行 COMMIT 语句提交事务。
场景 2:没有显式开始的事务,一条 SQL 语句执行时,InnoDB 会隐式开始一个事务,SQL 语句执行实现之后,主动提交事务。
如果没有开启 binlog,SQL 语句扭转表中数据,不产生 binlog,不必保障 binlog 和表中数据的一致性,用户事务也就不须要应用二阶段提交了。
InnoDB 内部事务是个特例,不论是否开启了 binlog,扭转表中数据都不会产生 binlog 日志,所以内部事务不须要应用二阶段提交。
2. prepare 阶段
以下代码中,ha_prepare_low() 会调用 binlog 和 InnoDB 解决 prepare 逻辑的办法。
int MYSQL_BIN_LOG::prepare(THD *thd, bool all) {
...
thd->durability_property = HA_IGNORE_DURABILITY;
...
int error = ha_prepare_low(thd, all);
...
}
调用 ha_prepare_low() 之前,用户线程对象的 durability_property
属性值会被设置为 HA_IGNORE_DURABILITY
。
这个属性和 redo 日志刷盘无关,InnoDB prepare 会用到。
2.1 binlog prepare
binlog 被看作一种存储引擎,它也有 prepare 阶段,代码如下:
// sql/binlog.cc
static int binlog_prepare(handlerton *, THD *thd, bool all) {
DBUG_TRACE;
if (!all) {thd->get_transaction()->store_commit_parent(mysql_bin_log.m_dependency_tracker.get_max_committed_timestamp());
}
return 0;
}
二阶段提交时,all = true,不会命中分支 if (!all)
。也就是说,在 prepare 阶段,binlog 什么也不会干。
2.2 InnoDB prepare
二阶段提交的 prepare 阶段,InnoDB 次要做五件事。
第 1 件,把调配给事务的所有 undo 段的状态从 TRX_UNDO_ACTIVE
批改为 TRX_UNDO_PREPARED
。
进入二阶段提交的事务,都至多扭转过(插入、更新、删除)一个用户表的一条记录,起码会调配 1 个 undo 段,最多会调配 4 个 undo 段。
具体什么状况调配多少个 undo 段,后续对于 undo 模块的文章会有具体介绍。
不论 InnoDB 给事务调配了几个 undo 段,它们的状态都会被批改为 TRX_UNDO_PREPARED。
第 2 件,把事务 Xid 写入所有 undo 段中以后提交事务的 undo 日志组头信息。
InnoDB 给以后提交事务调配的每个 undo 段中,都会有一组 undo 日志属于这个事务,事务 Xid 就写入 undo 日志组的头信息。
对于第 1、2 件事,如果事务扭转了用户一般表的数据,批改 undo 段状态、把事务 Xid 写入 undo 日志组头信息,都会产生 redo 日志。
第 3 件,把内存中的事务对象状态从 TRX_STATE_ACTIVE
批改为 TRX_STATE_PREPARED
。
后面批改 undo 状态,是为了事务提交实现之前,MySQL 解体了,下次启动时,可能从 undo 段中复原解体之前的事务状态。
这里批改事务对象状态,用于 MySQL 失常运行过程中,标识事务曾经进入二阶段提交的 prepare 阶段。
第 4 件 ,如果以后提交事务的隔离级别是 读未提交 (READ-UNCOMMITTED
)或 读已提交(READ-COMMITTED
),InnoDB 会开释事务给记录加的共享、排他 GAP 锁。
尽管读未提交、读已提交隔离级别个别都只加一般记录锁,不加 GAP 锁,然而,外键束缚查看、插入记录反复值查看这两个场景下,还是会给相应的记录加 GAP 锁。
第 5 件,调用 trx_flush_logs(),解决 redo 日志刷盘的相干逻辑。
static void trx_flush_logs(trx_t *trx, lsn_t lsn) {
...
switch (thd_requested_durability(trx->mysql_thd)) {
case HA_IGNORE_DURABILITY:
/* We set the HA_IGNORE_DURABILITY
during prepare phase of binlog group commit
to not flush redo log for every transaction here.
So that we can flush prepared records
of transactions to redo log in a group
right before writing them to binary log
during flush stage of binlog group commit. */
break;
case HA_REGULAR_DURABILITY:
...
trx_flush_log_if_needed(lsn, trx);
}
}
从名字上看,trx_flush_logs() 的作用是把事务产生的 redo 日志刷盘。
后面介绍过,MYSQL_BIN_LOG::prepare() 调用 ha_prepare_low()
之前,就曾经把以后事务所属用户线程对象的 durability_property
属性设置为 HA_IGNORE_DURABILITY
了。
从下面的代码能够看到,用户线程对象的 durability_property
属性值为 HA_IGNORE_DURABILITY
,prepare 阶段并不会把 redo 日志刷盘。
3. 总结
开启 binlog 的状况下,用户事务须要应用二阶段提交来保障 binlog 和 InnoDB 表的数据一致性。
binlog prepare 什么也不会干。
InnoDB prepare 会把调配给事务的所有 undo 段的状态批改为 TRX_UNDO_PREPARED,把事务 Xid 写入 undo 日志组的头信息,把内存中事务对象的状态批改为 TRX_STATE_PREPARED。
本期问题:二阶段提交的 prepare 阶段为什么不把 redo 日志刷盘?欢送大家留言交换。
下期预报:MySQL 外围模块揭秘 | 08 期 | 二阶段提交 (2) commit 阶段。