根据MySQL 5.6:事务模型,我们知道一个事务的执行分为三个阶段:事务开始 / 事务执行 / 事务提交,符合于二阶段提交

MySQL 5.5

在MySQL 5.6以前,通过臭名昭著的prepare_commit_mutex锁来实现Binlog中事务的记录顺序事务的提交顺序是一致的,每次只能fsync一个事务的Binlog

  • 获取prepare_commit_mutex
  • InnoDB Prepare
  • Binlog Prepare (write and fsync binary log)
  • InnoDB Commit (Binlog Commit什么都不做)
  • 释放prepare_commit_mutex

因为:

  1. fsync的代价昂贵,磁盘每秒执行fsync的次数存在上限
  2. 每次fsync的数据量对fsync的耗时影响达不到线程增长

    MySQL 5.6

    MySQL 5.6中,去掉了prepare_commit_mutex,使用Group Commit的方式每次fsync“批量”事务的Binlog,Binlog和Redo日志记录的事务顺序一致的保证也在下文讨论
    参考MySQL 5.6:Binlog Group Commit
    Group Commit发生在【事务提交阶段】
    Binlog - Flush Stage
  • Leader线程根据Flush队列顺序将每个线程Binlog Cache中的日志写入MySQL公共Binlog Buffer中(IO_CACHE log_file)
  • 将MySQL公共Binlog Buffer中的日志写入Binlog日志文件中(内存)

Binlog - Sync Stage

  • Flush队列的线程按顺序进入到Sync队列
  • Leader线程将Binlog日志文件(内存)刷入磁盘

InnoDB - Commit Stage

  • Sync队列的线程按顺序进入到Commit队列
  • Leader根据Commit队列顺序调用存储引擎提交每个线程的事务

注:当前活跃线程是Leader,Follower线程全部休眠
那么,这里为什么要使用队列,使用集合表示一个“事务组”可以吗?
Flush / Sync / Commit队列里的线程具有相同的顺序,Binlog中事务的记录顺序事务的提交顺序是一致的,可以使Binlog里不会出现“提交事务空洞”,方便崩溃恢复

之前认为队列的作用是为了保证Binlog和Redo日志记录的事务顺序一致,但在组提交阶段的Redo日志已经记录完成。这个顺序一致是通过行锁来保证
事务T1:UPDATE id SET id=id+1
事务T2:UPDATE id SET id=id*2
如果:

  • Redo日志记录顺序是:T1,T2
  • Binlog记录顺序是:T2,T1

会导致Master/Slave上id的值不一样。这种场景会发生吗?
T1,T2具有行锁冲突,当T1没有提交之前不会释放行锁,因此T2不可能也进入组提交阶段
其实只需要保证对于Binlog和Redo日志,事务组(队列)间保证顺序一致,事务组内不需要保证顺序一致(基于组提交的并行复制的原理,参考MySQL 5.7:并行复制

Group Commit的作用

  1. 减少事务的平均执行时间
    这里一定要注意,Group Commit增加了每个事务的执行时间,但减小了所有事务的平均执行时间

【错误观点】
MySQL 5.5(Group Commit之前),一个事务的耗时 = 事务执行时间 + fsync(Binlog) + fsync(Redo)
MySQL 5.5之后(Group Commit),一个事务的耗时 = 事务执行时间 + fsync(Binlog) / N + fsync(Redo)
但其实Group Commit队列中的每一个事务:

  • 首先会等待队列为满
  • 再会等待Leader执行完Flush / Sync / Commit三个阶段后才会被唤醒(这部分时间与MySQL 5.5中一个事务的耗时相同)

所以,每个事务的耗时都增加了,但因为“时间叠加”的原因,导致平均耗时缩短
2. 降低“写放大”问题
磁盘的写入单元是扇区(机械磁盘是512K,SSD是4M),无论一次写入数据量多少都会与扇区“对齐”,比如想写入10B,但也需要写入512K,这就是“写放大”问题
因此一次fsync的数据量越多,更能降低“写放大”问题

InnoDB的Group Commit

或者说,Redo日志有没有Group Commit的效用,即可以一次fsync“批量”事务的Redo日志 ?

Binlog开启时

MySQL 5.7.6之前
根据MySQL 5.6:事务模型,Redo日志的刷盘在【InnoDB Prepare】阶段中,即每个事务都会将自身产生的Redo日志刷写到磁盘中

MySQL 5.7.6之后

进行了InnoDB Group Commit的设计:

  • 在Flush Stage阶段,在每个线程产生的Binlog刷入磁盘前,Leader将当前公共Redo Buffer的内容“批量”写入磁盘

注意,在现有的InnoDB Crash Recovery逻辑下,Redo日志的落盘一定要早于Binlog
设想,如果Redo日志的落盘晚于Binlog,那么可能会出现这样的情况:在Binlog中找到事务T的xid,但T的Redo日志没有完全落盘,因此InnoDB无法提交事务T(目前的InnoDB Crash Recovery逻辑,InnoDB需要提交事务T)

正确性

这里需要保证队列中所有的线程产生的Redo日志都被写入磁盘(因为接下来的Commit Stage中所有线程都被标记为已提交),在公共Redo Buffer中刷写到的位置是log_sys->lsn
log_sys->lsn是在【事务执行阶段】,一个MTR执行结束(mtr的commit)将私有的Redo日志写入到公共Redo Buffer中,会更新log_sys->lsn
在组提交的Flush Stage中,已进入【事务提交阶段】,即每个在Flush Stage阶段的事务产生的Redo日志已写入公共Redo Buffer,所以log_sys->lsn已经包含每个处于Flush Stage阶段的事务

Binlog关闭时

Binlog关闭后,TC_LOG(Transcation Coordinator Log)是TC_LOG_DUMMY
Binlog关闭后,事务的提交不再是两阶段
innobase_commit主要为以下两步:

  • innobase_commit_low(trx):内存操作(设置事务状态为TRX_STATE_COMMITTED_IN_MEMORY,这里违反了WAL),由MTR产生LSN,传入trx_commit_complete_for_mysql写盘时使用
  • trx_commit_complete_for_mysql(trx):磁盘操作(将此次提交产生的Redo日志写入磁盘)

在一些场景下可能会产生“批量”写盘

  • Trx1 - innobase_commit_low:产生LSN1
  • Trx2 - innobase_commit_low:产生LSN2(LSN2 > LSN1)
  • Trx2 - trx_commit_complete_for_mysql:顺带将Trx1的Redo日志刷入磁盘
  • Trx1 - trx_commit_complete_for_mysql:发生Redo日志已刷入磁盘,什么都不做

需要注意的是innobase_commit违反了WAL规则,在lock_trx_release_locks函数中:
因为ordered_commit是在MYSQL_BIN_LOG::commit里调用,考虑可以将ordered_commit的部分逻辑移植到TC_LOG_DUMMY->commit中

参考

  1. InnoDB—-深入理解事务提交–01
  2. Efficient group commit for binary log