MySQL事务组提交 - saviochen/mysql_docs GitHub Wiki

1 事务提交的顺序

MySQL 的内部 XA 机制保证了单个事务在 binlog 和 InnoDB 之间的原子性,接下来我们需要考虑,在多个事务并发执行的情况下,怎么保证在 binlog 和 redo 中的顺序一致?

2 早期解决方法

在 MySQL 5.6 版本之前,使用 prepare_commit_mutex 对整个 2PC 过程进行加锁,只有当上一个事务 commit 后释放锁,下个事务才可以进行 prepare 操作,这样完全串行化的执行保证了顺序一致。 存在的问题是,prepare_commit_mutex 的锁机制会严重影响高并发时的性能,在每个事务执行过程中, 都会至少调用 3 次刷盘操作(写 redo,写 binlog,写 commit),多个小 IO 是非常低效的方式。

3 MySQL 5.6 组提交

为了提高并发性能,肯定要细化锁粒度。MySQL 5.6 引入了 binlog 的组提交(group commit)功能,prepare 阶段不变,只针对 commit 阶段,将 commit 阶段拆分为三个过程:

  1. flush stage:多个线程按进入的顺序将 binlog 从 cache 写入文件,但不刷盘。
  2. sync stage:将多个线程的 binlog 合并一次刷盘,对 binlog 文件做 fsync操作。
  3. commit stage:各个线程按顺序做 InnoDB commit 操作。

其中,每个阶段有 lock 进行保护,因此保证了事务写入的顺序。实现方法是,在每个 stage 设置一个队列,第一个进入该队列的线程会成为 leader,后续进入的线程会阻塞直至完成提交。leader 线程会领导队列中的所有线程执行该 stage 的任务,并带领所有 follower 进入到下一个 stage 去执行,当遇到下一个 stage 为非空队列时,leader 会变成 follower 注册到此队列中。

这种组提交的优势在于锁的粒度减小,三个阶段可以并发执行,从而提升效率。

4 MySQL 5.7 组提交优化

核心思想:延迟写 redo 到 group commit 阶段。

MySQL 5.6 的组提交逻辑中,每个事务各自做 prepare 并写 redo,只有到了 commit 阶段才进入组提交,因此每个事务的 redo sync 操作成为性能瓶颈。 在 5.7 版本中,修改了组提交的 flush 阶段,在 prepare 阶段不再让线程各自执行 flush redo 操作,而是推迟到组提交的 flush 阶段,flush stage 修改成如下逻辑:

  1. 收集组提交队列,得到 leader 线程,其余 follower 线程进入阻塞。
  2. leader 调用 ha_flush_logs 做一次 redo write/sync,一次性将所有线程的 redo 刷盘。
  3. 将队列中 thd 的所有 binlog cache 写到 binlog 文件中。

优化后两阶段提交的步骤变为:prepare【innodb层不再write/sync redo】-> flush【把innodb层write/sync redo的工作移动至此,将binlog cache写入binlog文件】->sync【将多个线程的 binlog 合并一次刷盘,对 binlog 文件做 fsync操作】->commit【各个线程按顺序做 InnoDB commit 操作】

这个优化是将 redo 的刷盘延迟到了 binlog group commit 的 flush stage 之中,sync binlog 之前。通过延迟write/sync redo 的方式,为 redo 做了一次组写入,这样 binlog 和 redo 都进行了优化。