什么是组提交?
基本的想法是:
- 多个线程,每一个线程处理一个事务.
- 当每个事务都执行完Prepare-Commit大部分操作,需要写盘时,排队等待日志文件(此处不指明具体实现)
- 日志文件可以写后,由一个线程执行批量写盘
- 写盘完成后,写盘线程唤醒其余线程,这些线程返回“成功”标识
几个相关的系统变量
sync_binlog
控制binlog的写盘方式
- 0:MySQL不控制binlog的写盘,由文件系统控制文件缓存区的写盘
- 1:每次事务提交,都会进行一次写
-
innodb_flush_log_at_trx_commit
控制redo log的写盘方式
0:log buffer每秒写入文件并写入到磁盘
- 1:每次事务提交时,log buffer会写入日志文件,并写入磁盘
2:每次事务提交时,log buffer会写入日志文件,每秒一次将日志文件写入到磁盘
innodb_support_xa
在事务中是否支持两阶段提交协议
0:不支持
-
约束
组提交需要保证如下约束:
【约束1】binlog和存储引擎具有一致的事务提交顺序
- 【约束2】提交的事务具有持久性
【约束1】基于如下的一些考虑:
- 【考虑1】在事务提交中,推迟存储引擎层的fsync调用,当出现故障后,可以由binlog从特定的点(point)回放来提交丢失的事务(这种场景可能不必须要求一致的提交顺序,但一致的提交顺序是一种简单的实现)
- 【考虑2】使用binlog来进行Slave同步时,一致的事务提交顺序肯定能够保证Master/Slave的数据一致性
- ……
【约束2】是具有事务保障的数据库系统的通用约束,持久性(Durable)是ACID中的“D”,意味着当一个事务成功提交后,即使系统崩溃重启后,依然可以看到事务已经提交
实现
让我们看看没有组提交时期的事务提交过程实现
1. 为了保障【约束1】持久性,通常的做法是使用持久化日志的方法,在系统崩溃重启后,通过持久化的日志来恢复崩溃前的状态(持久化的日志还可以用来建立Slave,来提高可用性和可扩展性,例如使用binlog在Slave上进行事务的回放)
在事务执行过程中,对binlog和存储引擎采用阶段提交协议(2 - PC)可以保障事务的原子性、一致性和持久性
当开启binlog后的两阶段提交流程:
【两阶段提交】
- Prepare - 1(innobase_xa_prepare):事务在存储引擎内持久化,此时的事务依然可以被回滚
- Prepare - 2(write and fsync binary log):如果Prepare - 1成功,事务在binlog内持久化
- Commit(innobase_commit):事务在存储引擎内提交,即事务不再可以被回滚
可以证明,在以上任意阶段的任意故障,都可以保证事务的:
- 原子性:对于执行失败的事务,回滚或者重新提交
- 一致性:在崩溃恢复时,若binlog和存储引擎崩溃前的状态一直,可以通过回滚或者重新提交保障两者状态的一致性(比如binlog提交成功,而存储引擎提交失败,那么崩溃恢复时,重新再存储引擎提交这个事务)
- 持久性:因为记录事务的日志持久化在磁盘上,因而可以用来进行崩溃恢复
- 为了保障【约束2】一致的事务提交顺序,在多个事务并发执行的场景下,使用prepare_commit_mutex。具体如下:
- 在Prepare - 1执行前,获取prepare_commit_mutex
- 在Commit执行后,释放prepare_commit_mutex
事务完全串行化执行,前一个事务Commit后下一个事务才能Prepare,并且在两阶段提交时,同时持久化到存储引擎和binlog,必然可以保障一致的提交顺序
这样在Prepare - 2对于binlog的fsync便只能执行当前事务的刷盘,无法实现批量事务的刷盘
注:如果prepare_commit_mutex提前释放,比如在Prepare - 2阶段释放,那么可能导致:
【例1】
- 事务A获取prepare_commit_mutex
- 事务A完成Prepare - 1,Prepare - 2(事务A在binlog内持久化)
- 事务A释放prepare_commit_mutex
- 事务B获取prepare_commit_mutex
- 事务B完成Prepare - 1,Prepare - 2(事务B在binlog内持久化)
- 事务B释放prepare_commit_mutex
- 事务B完成Commit(事务B在存储引擎内持久化)
- 事务A完成Commit(事务A在存储引擎内持久化)
binlog的提交顺序:AB,存储引擎的提交顺序:BA,出现了提交顺序不一致
【组提交】
组提交将Commit过程分成 Flush Stage、Sync Stage、Commit Stage 三个阶段
每个Stage维护一个队列:
- 如果当一个线程进入到一个空队列时,该线程就做为该队列的 Leader
- 否则该线程均为 Follower
- Leader带领所有Follower完成该队列的任务,并带领所有Follower进入下一个队列
- 如果下一个队列非空,Leader降级为Follower
- 如果下一个队列为空,Leader继续为Leader
通过如下两个方法在保障【约束1】【约束2】的同时,完成binlog的组提交(根据【约束1】的【考虑1】,只需要实现binlog的组提交,redo log则可以异步写盘)
- 减小锁的粒度:批量事务的binlog刷盘
队列:在没有全程锁保护的情况下,保障一致的提交顺序队列的思想可以这么举例,比如在【例1】中释放prepare_commit_mutex后的事务加入Commit队列,防止A先释放锁,却B会先于A进行Commit
事务提交阶段
Binlog - Flush Stage
根据队列顺序将每个线程Binlog Cache中的日志写入MySQL公共Binlog Buffer中(IO_CACHE log_file)
- 将MySQL公共Binlog Buffer中的日志写入Binlog日志文件中(内存)
【注意】Leader会在Flush Stage的队列里等待binlog_max_flush_queue_time时长,来保证队列内有一定量的Follower,以”批量化”,但默认binlog_max_flush_queue_time为0,即Leader会一直读取队列中的内容(同时还有THD作为Follower加入)直至队列尾
Binlog - Sync Stage
- Binlog日志文件(内存)刷入磁盘
- sync_binlog=N时会等待N个组,再将N个组产生的的Binlog刷写入磁盘
InnoDB - Commit Stage
- 根据队列顺序调用存储引擎提交事务
其中:
- Redo Log的写盘时间由innodb_flush_log_at_trx_commit变量控制
- Binlog的写盘时间由sync_binlog变量控制
每个队列由Mutex_queue结构体描述,由成员变量m_first / m_last指向队列头 / 尾,由THD->next_to_commit来指向队列中下一个元素
举例
Prepare:
- T1先进入Prepare阶段,并完成Prepare阶段,先进入Flush Stage队列,并成为Leader
- T2随后进入Prepare阶段,随后完成Prepare阶段,进入Flush Stage队列
Flush Stage:
- Flush Stage的Leader(T1)带领所有Followers(T2)进入到Sync Stage队列
Sync Stage:
- Sync Stage为空,T1继续为Leader,T2继续为Follower
- Leader(T1)带领Follower(T2)进行一次fsync操作,T1先于T2刷入磁盘
- Flush Stage的Leader(T1)带领所有Followers(T2)进入到Commit Stage队列
Commit Stage:
- Commit Stage为空,T1继续为Leader,T2继续为Follower
- Leader(T1)带领Follower(T2)进行一次存储引擎Commit操作,T1先于T2在存储引擎内提交
这里有很关键的一点是:将Commit阶段细分解为三个过程后:
- Flush
- Sync
- Commit
细分为这三个过程后,使得事务可以实现部分的并行化(比如T1在Flush时,T2就可以Prepare)
MySQL 5.7:基于Group Commit的并行复制
能同时进入Prepare队列的事务说明SQL的执行没有锁冲突(SQL语句可以并行执行,T1的SQL语句在执行时,T2的SQL语句也可以在执行时)
- 为这一组事务分配一个相同的last_commited序号,last_commited相同表明这组事务在Master上是同时存在于Prepare阶段
- 并按照事务在队列中的顺序分配一个sequence_number序号,sequence_number在Master全局递增
具有相同last_commited序号的事务在Slave上是可以并行复制的(SQL语句的执行没有锁冲突,可以视为“无关联”,比如就是很简单的针对不同表的INSERT操作而已)
参考
- Kristian Nielsen,Fixing MySQL group commit Part1,Part2, Part3, Part4
- MariaDB WL#116
- MySQL 组提交
- MySQL Bug #13669
- MySQL并发复制系列三:MySQL和MariaDB实现对比