二阶段提交的 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 日志刷盘。
二阶段提交的 commit 阶段,分为三个子阶段。
flush 子阶段,要干两件事:
第 1 件
,触发操作系统把 prepare 阶段及之前产生的 redo 日志刷盘。
事务执行过程中,改变(插入、更新、删除)表中数据产生的 redo 日志、prepare 阶段修改 undo 段状态产生的 redo 日志,都会由后台线程先写入 page cache,再由操作系统把 page cache 中的 redo 日志刷盘。
等待操作系统把 page cache 中的 redo 日志刷盘,这个时间存在不确定性,InnoDB 会在需要时主动触发操作系统马上把 page cache 中的 redo 日志刷盘。
上一篇文章,我们介绍过,二阶段提交的 prepare 阶段不会主动触发操作系统把 page cache 中的 redo 日志刷盘。这个刷盘操作会留到 flush 子阶段进行。
第 2 件
,把事务执行过程中产生的 binlog 日志写入 binlog 日志文件。
这个写入操作,也是先写入 page cache,至于操作系统什么时候把 page cache 中的 binlog 日志刷盘,flush 子阶段就不管了。
sync 子阶段,根据系统变量 sync_binlog 的值决定是否要触发操作系统马上把 page cache 中的 binlog 日志刷盘。
commit 子阶段,完成 InnoDB 的事务提交。
3. 组提交
flush 子阶段会触发 redo 日志刷盘,sync 子阶段可能
会触发 binlog 日志刷盘,都涉及到磁盘 IO。
TP 场景,比较常见
的情况是事务只改变(插入、更新、删除)表中少量数据,产生的 redo 日志、binlog 日志也比较少。
我们把这种事务称为小事务
。
以 redo 日志为例,一个事务产生的 redo 日志少,操作系统的一个页就有可能存放多个事务产生的 redo 日志。
如果每个事务提交时都把自己产生的 redo 日志刷盘,共享操作系统同一个页存放 redo 日志的多个事务,就会触发操作系统把这个页多次刷盘。
数据库闲的时候,把操作系统的同一个页多次刷盘,也没啥问题,反正磁盘闲着也是闲着。
数据库忙的时候,假设某个时间点有 1 万个小事务要提交,每 10 个小事务共享操作系统的一个页用于存放 redo 日志,总共需要操作系统的 1000 个页。
1 万个事务各自提交,就要触发操作系统把这 1000 个数据页刷盘 10000 次。
根据上面假设的这个场景,我们可以看到,这些事务都在某个时间点提交,可以等到共享操作系统同一个页的事务把 redo 日志都写入到 page cache 之后,再触发操作系统把 page cache 的这一个页刷盘。
这样一来,1000 个数据页,只刷盘 1000 次就可以了,刷盘次数只有原来的十分之一,效率大大的提高了。
上面是以 redo 日志为例描述操作系统的同一个页重复刷盘的问题,binlog 日志也有同样的问题。
某个时间点提交的多个事务触发操作系统的同一个页重复刷盘,这是个问题,为了解决这个问题,InnoDB 引入了组提交。