两阶段提交

在讲述MySQL的事务模型之前,有必要介绍一下两阶段提交,参考《Designing Data-Intensive Application》

  • 【1】应用程序通知协调者开始事务,并告知协调者事务的内容
  • 【2】协调者给每个参与者发送单节点的读/写事务(对应于图中两个write data)
  • 【3】协调者给每个参与者发送Prepare请求
  • 【4】如果参与者回复“Yes”,意味着参与者放弃自身中止事务的权力,无论出现何种状况也必须自身提交事务
  • 【5】协调者收到所有参与者的回复后,做出最终决定(只有所有回复都是“Yes”,最终决定才是“Commit”)
  • 【6】如果协调者最终决定是“Commit”,意味着无论发生何种状况,协调者也要保证所有参与者都提交成功

两阶段提交的原子性由【4】和【6】保证,称作“Two Points of No Return”
注意:在【2】【3】间,协调者没必要再去询问每一位参与者“【1】中的读/写事务有没有执行成功?”,因为如果有参与者在执行失败的话,可以在收到Prepare请求时回复“No”

  • 称【1】为事务开始阶段
  • 称【2】为事务执行阶段
  • 称【3】-【6】为事务提交阶段

    • 【3】【4】也叫Prepare阶段
    • 【5】【6】也叫Commit阶段

      MySQL的事务模型

      事务开始

      注意,下面的列表由上至下表示由先到后的执行顺序
  • 检查是否有活跃的事务还未提交,如果有的话则调用ha_commit_trans提交之前的事务,并释放之前事务持有的MDL锁

  • 开启一个事务,事务状态为ACTIVE

    事务执行

  • 【InnoDB】

    • 产生Undo(内存)日志,产生“修改Undo日志行为”的Redo(内存)日志
    • MTR:修改内存中的数据页,产生“修改数据页行为”的Redo(内存)日志
    • 将修改的数据页(脏页)加入Flush List中(Cleaner线程异步地刷脏页到磁盘的数据文件中)
    • 将线程产生的私有的Redo日志提交到公共的Redo Log Buffer
  • 【Server】
    • 产生Binlog(内存)日志
  • 【语句提交】
    • 【Binlog Prepare】binlog_prepare:什么都不做
    • 【InnoDB Prepare】innobase_xa_prepare:释放可能持有的自增键锁
    • 【Binlog Commit】binlog_commit:什么都不做
    • 【InnoDB Commit】innobase_commit:同【InnoDB Prepare】

可以看到,在事务执行阶段,MySQL加入了“语句提交”这一两阶段提交过程,但是Binlog在Prepare和Commit阶段的内容都为空,因此实质上只是InnoDB的单节点提交

事务提交

  • 【Binlog Prepare】binlog_prepare:什么都不做
  • 【InnoDB Prepare】innobase_xa_prepare:
    • 【1】将事务状态置为PREPARED
    • 【2】将Redo日志写入磁盘
  • 【组提交】ordered_commit:

    • 【1】Flush Stage: 将Binlog内容写入Binlog日志文件(内存,尚未写盘)
    • 【2】Sync Stage: 将Binlog文件写盘
    • 【3】Commit Stage:存储引擎提交(此处以InnoDB为例)
      • 清理insert_undo日志
      • 将update_undo日志加入回滚段的History-List(Purge线程定期清理)
      • 释放事务持有的所有锁(比如,修改同一行记录的事务需要有互斥锁)
      • 清理(如果有的话)Savepoint列表
      • 内存中事务结构体的状态置为COMMITTED(准确的将是TRX_STATE_COMMITTED_IN_MEMORY**)**
      • Undo日志中事务的状态TRX_UNDO_CACHED / TRX_UNDO_TO_FREE / TRX_UNDO_TO_PURGE(在Crash Recovery时,读到这三种状态都会将事务内存结构置为TRX_STATE_COMMITTED_IN_MEMORY

        MySQL的事务提交是不是标准的两阶段提交?

        可以看到,【组提交】中的【1】【2】为Binlog提交,【3】为InnoDB提交,因此MySQL的事务提交阶段是两阶段提交,我们来看一下是怎么保证“Two Points of No Return”:
  • Binlog Prepare什么都不做,Binlog确信自身一定可以把Binlog内容写入磁盘

  • InnoDB Prepare将Redo日志写入磁盘,则InnoDB一定可以成功提交,其实此时就可以认为事务在InnoDB中提交成功了

但注意一点,在两阶段提交算法中,如果存在参与者返回“No”,协调者是需要再向所有参与者发送“Abort”命令,中止事务,使所有参与者恢复到到事务开始之前的状态,在MySQL里是如何实现的?

  • 当存在协调者返回“No”:Binlog Prepare或者InnoDB Prepare出错,(一般来讲)MySQL Crash
  • 协调者发送“Abort”命令中止事务,使所有参与者恢复到事务开始之前的状态:此时相当于进行到【Binlog Prepare】或者【InnoDB Prepare】,事务状态为Active / Prepared,但是Binlog尚未写入磁盘(Binlog在【组提交】时才写入磁盘),根据MySQL Crash Recovery的原理,这个事务会被回滚(Binlog层和InnoDB层都会回滚,但Binlog层可能不需要做什么处理)

这里有一个问题,如果进行到【InnoDB Prepare】,并且Redo日志已写入磁盘,事务还会被回滚吗?答案是会被回滚,就是做Undo操作,此时Redo日志看起来像这样:

2PC MySQL
参与者会返回“No” Binlog Prepare或者InnoDB Prepare出错,(一般来讲)导致MySQL Crash
参与者确认能Commit,返回“Yes”
- Binlog未做确定性保障
- InnoDB将Redo日志写盘,事务在引擎层已经持久化
协调者发送“Abort”命令中止事务 根据MySQL Crash Recovery的原理,这个事务会被回滚(Binlog层和InnoDB层都会回滚,但Binlog层可能不需要做什么处理)

如果一个参与者回复“Yes”却提交失败,这是两阶段提交不允许的。但是MySQL依然可以通过崩溃恢复来保证所有参与者(Binlog和InnoDB)的数据一致性

不用两阶段提交行不行?

如果不用两阶段提交,把执行过程顺序写下来:
【过程1】

  • 【InnoDB - 1】将事务状态置为Prepare
  • 【InnoDB - 2】将Redo日志写入磁盘(猜测这里已经将事务执行中生成的Redo日志持久化到磁盘了)
  • 【Server - 1】Flush Stage: 将Binlog内容写入Binlog日志文件(内存,尚未写盘)
  • 【Server - 2】Sync Stage: 将Binlog文件写盘
  • 【InnoDB - 3】Commit Stage:存储引擎提交(此处以InnoDB为例)
    • 清理Undo日志(Purge线程异步清理)
      • INSERT的Undo日志直接删除(INSERT数据行的回滚是“空”)
      • UPDATE的Undo日志需要放到History List上,等待Purge线程根据事务可见性删除
    • 释放事务持有的所有锁(比如,修改同一行记录的事务需要有互斥锁)
    • 清理(如果有的话)Savepoint列表
  • 【InnoDB - 4】事务状态置为Commit

注意,这依然是两阶段提交(比如执行完【InnoDB - 1,2】才能执行【Server - 1,2】),只是不显示的指明哪些是InnoDB的Prepare了,哪些是Binlog的Commit之类的了。那我们把顺序变一变:
【过程2】

  • 【Server - 1】Flush Stage: 将Binlog内容写入Binlog日志文件(内存,尚未写盘)
  • 【Server - 2】Sync Stage: 将Binlog文件写盘
  • 【InnoDB - 1】将事务状态置为Prepare
  • 【InnoDB - 2】将Redo日志写入磁盘(猜测这里已经将事务执行中生成的Redo日志持久化到磁盘了)
  • 【InnoDB - 3】Commit Stage:存储引擎提交(此处以InnoDB为例)
    • 清理Undo日志(Purge线程异步清理)
    • 释放事务持有的所有锁(比如,修改同一行记录的事务需要有互斥锁)
    • 清理(如果有的话)Savepoint列表
  • 【InnoDB - 4】事务状态置为Commit

这就不是两阶段提交了,需要Server层全部执行完,再执行InnoDB层。那么,这会有问题吗?举个例子,Server层执行成功,InnoDB层执行失败Crash:

  • Recovery时可以在Binlog中查找事务内容,InnoDB重新执行

这个方法类似于在一阶段提交时,协调者对失败的参与者发起(无限)重试请求。见下图,由于一阶段提交对于重试次数的上限无法保证(两阶段提交可以看做是重试次数上限为“2”的一阶段提交),结论是:一阶段提交平均来讲,会增大服务不可用时间数据不一致时间

具体到MySQL呢?比如在【InnoDB-2】将Redo日志写入磁盘操作失败

  • 【过程1】对Binlog和InnoDB尚未产生任何影响
  • 【过程2】Binlog已写入文件(一个参与者已经提交成功),导致两个参与者的数据开始不一致(在实际场景下,Binlog和InnoDB的数据不一致表示为从库和主库的数据不一致)