根据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
因为:
- fsync的代价昂贵,磁盘每秒执行fsync的次数存在上限
- 每次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的作用
- 减少事务的平均执行时间
这里一定要注意,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中