回想当年,高并发还没有这么普遍,分布式也没有这么流行。
初次接触二阶段提交,源于想以事务的方式实现对 MongoDB 中多个集合数据的修改,而 MongoDB 本身不支持事务,官方推荐的方案就是使用二阶段提交。
MySQL 和事务早已成为工作中不可或缺的一部分,随着分布式的流行,二阶段提交出现在视野中的次数也越来越多。
然而,MySQL、事务、二阶段提交这 3 个名词组合在一起成为一个整体,从第一次接触到现在也不过一年时间。
第一次接触到MySQL 事务二阶段提交这个概念时,心里还有点小激动。
因为当年研究 MongoDB 二阶段提交,其实是没有弄明白的。
没想到,多年以后,在 MySQL 中发现了二阶段提交的身影,心头似乎涌现出了那种感觉:众里寻 TA 千百度,蓦然回首,那人却在灯火阑珊处。
本文我们就一起来看看 MySQL 事务是怎么实现二阶段提交的。
正文
1、什么是二阶段提交?
二阶段提交是一种用于保证分布式事务原子性的协议。
二阶段提交的实现过程,有 2 个角色参与其中:
资源管理器 ,Resource Manager,负责管理一部分资源。对于数据库来说,这里的资源指的就是数据。
如果把分布式事务看成是一个整体,每个资源管理器会负责其中的一部分,也就是分布式事务的一个本地事务。
资源管理器在分布式事务中的角色就是干活的,所以,我们可以称它为执行器。
事务管理器 ,Transaction Manager,负责管理分布式事务,协调事务的提交、回滚、以及崩溃恢复。
事务管理器在分布式事务中,就是那个总揽全局、指挥资源管理器干活的角色,所以,我们可以称它为协调器。
二阶段提交,顾名思义,会包含 2 个阶段:

prepare 阶段 ,协调器会询问所有执行器,是否可以提交事务。
此时,各个本地事务实际上已经执行完成,数据写入已经成功,就差提交这最后一哆嗦了。
如果有任何一个执行器因为它所执行的本地事务有问题不能提交,分布式事务就不能提交,协调器会通知所有执行器进行回滚操作。
如果每一个执行器都回复协调器可以提交,分布式事务就会进入下一个阶段,也就是commit 阶段。
commit 阶段 ,协调器会通知执行器进行提交操作。
执行器收到提交通知之后,各自提交自己的本地事务。
所有执行器都提交完成之后,二阶段提交就结束了,分布式事务也就执行完成了。
2、MySQL 二阶段提交场景
在 MySQL 中,二阶段提交有 4 种使用场景:
场景 1 ,外部 XA 事务,数据库中间件、应用程序作为协调器,MySQL 数据库实例作为执行器。
这种场景下,MySQL 通过以下 XA 系列命令来实现二阶段提交:
场景 2 ,单个 MySQL 实例的内部 XA 事务,没有开启binlog 日志,SQL 语句涉及多个支持事务的存储引擎。
TC_LOG_MMAP类对象作为协调器,多个支持事务的存储引擎作为执行器。
场景 3 ,单个 MySQL 实例的内部 XA 事务,没有开启binlog 日志,SQL 语句只涉及1 个支持事务的存储引擎。
这种场景下,原本是不需要二阶段提交的,但是为了统一,还是会以二阶段提交的结构进行提交操作。
TC_LOG_DUMMY类对象作为协调器,不记录 xid,存储引擎作为执行器。
场景 4 ,单个 MySQL 实例的内部 XA 事务,开启了binlog 日志,SQL 语句涉及1 个或多个支持事务的存储引擎。
MYSQL_BIN_LOG类对象作为协调器,分布式事务的xid记录在 binlog 日志文件中。binlog 日志和存储引擎作为执行器。
binlog 日志和存储引擎都是独立单元,为了保证多个存储引擎之间、存储引擎和 binlog 日志之间的数据一致性,在事务提交时,这些操作要么都提交,要么都回滚,需要借助 XA 事务实现。
InnoDB 是 MySQL 最常用的存储引擎,为了支持主从架构,binlog 日志也是必须要开启的,这是 MySQL 最常使用的场景。
接下来我们就以InnoDB 存储引擎 + binlog 日志为例,来介绍 MySQL 内部 XA 事务的二阶段提交过程
3、prepare 阶段
来到 prepare 阶段之前,InnoDB 对表中数据的写操作都已经完成,就差提交或者回滚这最后一哆嗦了。
prepare 阶段,binlog 日志和 InnoDB 主要干的事情有这些:
prepare 阶段,binlog 日志没有什么需要做的,InnoDB 主要做的事情就是修改事务和 undo 段的状态,以及记录xid(分布式事务的 ID)。
InnoDB 会把内存中事务对象的状态修改为TRX_STATE_PREPARED,把事务对应 undo 段在内存中的对象状态修改为TRX_UNDO_PREPARED。
修改完内存中各对象的状态,还不算完事,还要把事务对应 undo 段的段头页中 Undo Segment Header 的 TRX_UNDO_STATE 字段值修改为TRX_UNDO_PREPARED。
然后,把 xid 信息写入当前事务对应日志组的 Undo Log Header 中的 xid 区域。
修改 TRX_UNDO_STATE 字段值、写 xid,这两个操作都要修改 undo 页,修改 undo 页之前会先记录 Redo 日志。
4、commit 阶段
(1)commit 阶段整体介绍
到了 commit 阶段,一个事务就已经接近尾声了。
写操作(包括增、删、改)已经完成,内存中的事务状态已经修改,undo 段的状态也已经修改,xid 信息也已经写入 Undo Log Header,prepare阶段产生的 Redo 日志已经写入到 Redo 日志文件。
剩余的收尾工作,包括:
为了保证主从数据一致性,同一个事务中,上面列出的收尾工作必须串行执行。
Redo & binlog 日志刷盘都涉及到磁盘 IO,如果每提交一个事务,都把该事务中的 Redo 日志、binlog 日志刷盘,会涉及到很多小数据量的 IO 操作,频繁的小数量 IO 操作非常消耗磁盘的读写性能。
为了提升磁盘 IO 效率,从而提高事务的提交效率,MySQL 从 5.6 开始引入了 binlog 日志组提交功能,5.7 中把原本在 prepare 阶段进行的 Redo 日志刷盘操作迁移到了 commit 阶段。
binlog 日志组提交有何神奇之处,怎么就能提升磁盘 IO 效率呢?
引入 binlog 日志组提交功能之后,commit 阶段细分为 3 个子阶段。
对于每个子阶段,都可以有多个事务处于该子阶段,写日志 & 刷盘操作可以合并:
flush 子阶段 ,Redo 日志可以一起刷盘,binlog 日志不需要加锁就可以一起写入 binlog 日志文件。
sync 子阶段 ,binlog 日志可以一起刷盘。
commit 子阶段 ,Redo 日志可以一起刷盘。
通过合并 Redo 日志刷盘操作、合并 binlog 日志写入日志文件操作、合并 binlog 日志刷盘操作,把小数据量多次 IO 变为大数据量更少次数 IO,可以提升磁盘 IO 效率。
既然要合并 Redo、binlog 日志的写入、刷盘操作,那必须有一个管事的来负责协调这些操作。
如果引入一个单独的协调线程,会增加额外开销。
MySQL 的解决方案是把处于同一个子阶段的事务线程分为 2 种角色:
leader 线程管事的方式,并不是指挥 follower 线程干活,而是自己帮 follower 线程把活都干了。
commit 细分为 3 个子阶段之后,每个子阶段会有一个队列用于记录哪些事务线程处于该子阶段。
为了保证先进入 flush 子阶段的事务线程一定先进入 sync 子阶段,先进入 sync 子阶段的事务线程一定先进入 commit 子阶段,每个子阶段都会持有一把互斥锁。
接下来,我们一起来看看这 3 个子阶段具体都干了什么事情。
(2)flush 子阶段
flush 子阶段,第 1 个进入 flush 队列的事务线程,会成为 leader 线程。第 2 个及以后进入 flush 队列的事务线程,会成为 follower 线程。
follower 线程会进入等待状态,直到收到 leader 线程从 commit 子阶段发来的通知,才会醒来继续执行后续操作。
leader 线程会获取一把互斥锁,保证同一时间 flush 子阶段只有一个 leader 线程。
互斥锁保存到 MYSQL_BIN_LOG 类的Lock_log属性中,我们就叫它Lock_log 互斥锁好了。
flush 子阶段 leader 线程的主要工作流程如下:
第 1 步 ,清空 flush 队列。
清空之前,会先锁住 flush 队列,在这之前进入 flush 队列的所有事务线程就成为了一组。
锁住之后,会清空 flush 队列。
清空之后,进入 flush 队列的事务线程就属于下一组了,在这之后第 1 个进入 flush 队列的事务线程会成为下一组的 leader 线程。
有一点需要注意,当前组的 leader 线程持有的 Lock_log 锁要等到 sync 阶段才会释放。
如果下一组的 leader 线程在当前组的 leader 线程释放 Lock_log 锁之前就进入 flush 队列了,下一组的 leader 线程会阻塞,直到当前组的 leader 线程释放Lock_log 锁。
第 2 步 ,执行 Redo 日志刷盘操作,把 InnoDB 产生的 Redo 日志都刷新到磁盘。
第 3 步 ,遍历 leader 线程带领的一组 follower 线程,把 follower 线程中事务产生的 binlog 日志都写入到 binlog 日志文件。
每个事务在执行过程中产生的 binlog 日志都会先写入事务线程中专门用于存放该事务 binlog 日志的磁盘临时文件,这是事务线程中 binlog 日志的临时存放点。
等到二阶段提交的 flush 子阶段,才会按照事务提交的顺序,把每个事务产生的 binlog 日志从临时存放点拷贝到 binlog 日志文件中。
在第 1 步中,已经早早的把 flush 队列给清空了,还怎么遍历 leader 线程带领的一组 follower 线程呢?
别急,leader 线程既然作为管事的,它自然得知道它这一组中都有哪些 follower 线程。
每个线程对象(thd)中,都会有个Next_to_commit属性,指向紧随其后加入到 flush 队列的线程。
只要知道 leader 线程,根据每个线程的next_to_commit属性,就可以顺藤摸瓜找到 leader 线程带领的一组 follower 线程。
第 4 步 ,把 binlog 日志文件 IO_CACHE 中最后剩下的日志拷贝到 binlog 日志文件。
binlog 日志从临时存放点拷贝到 binlog 日志文件的过程中,得先写入 IO_CACHE,写满之后,才会把 IO_CACHE 中的日志拷贝到 binlog 日志文件。
第 3 步执行完成之后,binlog 日志文件的 IO_CACHE 可能没有写满,其中的日志也就不会被拷贝到 binlog 日志文件。
所以,第 4 步的存在就是为了把 binlog 日志文件 IO_CACHE 中最后剩下的不足 8K的日志拷贝到 binlog 日志文件。
(3)sync 子阶段
sync 子阶段也有一个队列,是sync 队列。
第 1 个进入 sync 队列的事务线程是 sync 子阶段的 leader 线程。
第 2 个及以后进入 sync 队列的事务线程是 sync 子阶段的 follower 线程。
flush 子阶段完成之后,它的 leader 线程会进入 sync 子阶段。
flush 子阶段的leader 线程来到 sync 子阶段之后,它会先加入 sync 队列,然后它的 follower 线程也会逐个加入 sync 队列。
flush 子阶段的 leader 线程和它的 follower 线程都加入到 sync 队列之后,leader 线程会释放它持有的 Lock_log 互斥锁。
如果 flush 子阶段的 leader 线程加入 sync 队列之前,sync 队列是空的,那么它又会成为 sync 子阶段的 leader 线程,否则,它和它的所有 follower 线程都会成为 sync 子阶段的 follower 线程。
在 sync 子阶段,依然是由 leader 线程完成各项工作。
follower 线程依然处于等待状态,直到收到 leader 线程从 commit 子阶段发来的通知,才会退出等待状态,执行后续操作。
leader 线程开展工作之前,会先获取Lock_sync 互斥锁,保证同一时间 sync 子阶段只有一个 leader 线程。
sync 子阶段 leader 线程的主要工作流程如下:
第 1 步 ,等待更多事务线程进入 sync 子阶段。
只有符合一定条件时,leader 线程才会进入等待过程。
介绍要符合什么条件之前,我们先来看看 leader 线程为什么要有这个等待过程?
前面介绍过,binlog 日志组提交就是为了把多个事务线程攒到一起,然后再把这些事务产生的 Redo 日志、binlog 日志一起刷盘,从而提升磁盘的 IO 效率。
leader 线程的等待过程,依然是为了把更多事务线程攒到一起,从而积攒更多 binlog 日志一起刷盘。
如果没有这个等待过程,第 1 个事务线程进入 sync 队列成为 leader 线程之后,它可不管有没有其它事务线程加入 sync 队列,就会马不停蹄的执行后面的流程。
数据库繁忙的时候,leader 线程开始执行后续流程之前,可能就有很多其它事务线程加入 sync 队列成为它的 follower 线程。
这种情况下,leader 线程有很多 follower 线程,它把这些 follower 线程的 binlog 日志一起刷盘,能够提升磁盘 IO 效率。
数据库不那么忙的时候,leader 线程开始执行后续流程之前,可能没有或者只有很少的事务线程加入 sync 队列成为它的 follower 线程。
这种情况下,leader 线程还是只能把少量的 binlog 日志一起刷盘,binlog 日志组提交功能提升磁盘 IO 效率就不那么明显了。
为了在数据库不那么忙的时候,也能尽量提升 binlog 日志组提交的效率,引入了 leader 线程的有条件等待过程,这个条件由系统变量sync_binlog控制。
sync_binlog = 0 ,MySQL 不会主动发起 binlog 日志刷盘操作。
只需要把 binlog 日志写入 binlog 日志文件的操作系统缓冲区,由操作系统决定什么时候执行刷盘操作。
sync_binlog = 1 ,sync 子阶段每一组的 leader 线程都会触发刷盘操作。
这意味着每个事务只要提交成功了,binlog 日志也一定刷新到磁盘了。
sync_binlog = 1 就是著名的双 1设置的其中一个 1。
sync_binlog = N ,sync 子阶段每 N 组才会触发一次刷盘操作。
也就是说,执行一次刷盘操作之后,接下来第 1 ~ N-1 组的 leader 线程都不会执行 binlog 日志刷盘操作。
等到第 N 组时,它的 leader 线程才会把第1 ~ N 组的所有事务线程产生的 binlog 日志一起刷盘。
源码中有一个变量sync_counter用于记录 sync 子阶段自上次刷盘操作以后,有多少组的 leader 线程没有进行刷盘操作。
每当有一个 leader 线程没有执行刷盘操作,sync_counter 变量的值就会加 1。
只要有一个 leader 线程执行了刷盘操作,sync_counter 变量的值就会清零,重新开始计数。
重点来了 ,如果某一组的 leader 线程判断sync_counter + 1 >= sync_binlog条件成立,那么该 leader 线程就要执行刷盘操作,刷盘之前会触发等待更多事务线程进入 sync 子阶段中的等待过程。
我们现在知道了 leader 线程为什么要等待,以及什么情况下需要等待,那要等待多长时间呢?
等待过程持续多长时间由 2 个系统变量控制。
binlog_group_commit_sync_delay,单位为微秒,表示 sync 子阶段的 leader 线程在执行 binlog 日志文件刷盘操作之前,需要等待的多少微秒,默认值为 0。
如果它的值为 0,表示跳过等待过程。
如果它的值大于 0,leader 线程会等待 binlog_group_commit_sync_delay 毫秒。
但是,在等待过程中,leader 线程会每隔一段时间就去看看 sync 队列里的事务线程数量是不是大于等于系统变量binlog_group_commit_sync_no_delay_count的值。
只要 binlog_group_commit_sync_no_delay_count sync 的值大于0,并且队列里的事务线程数量大于等于该系统变量的值,立马停止等待,开始执行第 2 步及之后的操作
第 2 步 ,清空 sync 队列。
清空之前,会先锁住 sync 队列,在这之前进入 sync 队列的所有事务线程就成为了一组。
锁住之后,会清空 sync 队列。
清空之后,进入 sync 队列的事务线程就属于下一组了,在这之后第 1 个进入 sync 队列的事务会成为下一组的 leader 线程。
第 3 步 ,binlog 日志文件刷盘。
刷盘操作完成后,这一组事务线程的 binlog 日志都刷新到磁盘,实现了持久化,它们再也不用担心数据库崩溃了。
在这一步是否会执行刷盘操作,也是由系统变量sync_binlog控制的,在第 1 步中已经详细介绍过,这里就不重复了介绍了。
(4)commit 子阶段
commit 子阶段也有一个队列,是commit 队列。
第 1 个进入 commit 队列的事务线程是 commit 子阶段的 leader 线程。
第 2 个及以后进入 commit 队列的事务线程是 commit 子阶段的 follower 线程。
sync 子阶段完成之后,它的 leader 线程会进入到 commit 子阶段,并加入 commit 队列,然后再让它的 follower 线程也逐个加入 commit 队列。
sync 子阶段的 leader 线程和 follower 线程都加入到 commit 队列之后,leader 线程会释放它持有的Lock_sync互斥锁。
如果 sync 子阶段的 leader 线程加入 commit 队列之前,commit 队列是空的,那么它又会成为 commit 子阶段的 leader 线程。
否则,它和它的 follower 线程都会成为 commit 子阶段的 follower 线程。
commit 子阶段,leader 线程最重要的工作就是提交事务,然后给所有处于 commit 子阶段的 follower 线程发通知。
leader 线程提交事务,是只提交自己的事务,还是会把所有 follower 线程的事务也一起提交了,由系统变量binlog_order_commits变量控制。
binlog_order_commits 的默认值为ON,表示 leader 线程除了会提交自己的事务,还会提交所有 follower 线程的事务。
如果 binlog_order_commits 的值为OFF,表示 leader 线程只会提交自己的事务。
leader 线程提交事务之后,会通知所有 follower 线程。
follower 线程收到通知之后,会退出等待状态,继续进行接下来的工作,也就是收尾工作。
每个事务线程中都有一个commit_low属性,如果 leader 线程已经把 follower 线程的事务也一起提交了,会把 follower 线程的该属性值设置为false,follower 线程在执行收尾工作的时候,就不需要再提交自己的事务了。
如果 leader 线程只提交了自己的事务,而没有提交 follower 线程的事务,commit_low属性的值为true,follower 线程在执行收尾工作的时候,需要各自提交自己的事务。
5、总结
二阶段提交的核心逻辑是把多个事务的 Redo 日志合并刷盘,把多个事务的 binlog 日志合并刷盘,从而把少量数据多次 IO 变为更大数据更少 IO,最终达到提升事务提交效率的目标。
最后,以一张二阶段提交的整体流程图作为本文的结尾:
mysql 多个事务同时提交 怎么执行
实际上事务本身是针对连接来说的,因此一个连接可能会多次进行事务操作,但是一个事务只连接一次数据库,无论有多少条数据库操作,也无论这些操作是不是select,insert,update等复合起来的
mysql把一个数据库中的数据复制到另一个数据库中的表 2个表结构相同
1、使用软件Navicat就可迁移复制数据库,打开Navicat,右键点击左边空白的地方,点击New Connection下的MySQL,创建一个服务器的连接,下面将演示把本地的数据迁移到服务器:2、在弹出的创建新连接的窗口里,输入服务器的IP,数据库账号,密码等,然后就可以连接数据库了:3、创建好后们打开本地的数据库,点击“Data Transfer”(数据传输),接着弹出新的界面:4、新窗口中在左边选择本地数据库的库,和需要转移的表,可以选择一个,或多个表:5、然后在右边的目标里,选择服务器的连接,然后选择服务器上的数据库:6、选择完成后,就开始进行数据转移了,数据量不是很大的,很快就会转移完成的。以上就是mysql中数据复制到另一个数据库的方法:
Mysql到底是怎么实现MVCC的
Mysql到底是怎么实现MVCC的Mysql到底是怎么实现MVCC的?这个问题无数人都在问,但google中并无答案,本文尝试从Mysql源码中寻找答案。 在Mysql中MVCC是在Innodb存储引擎中得到支持的,Innodb为每行记录都实现了三个隐藏字段:6字节的事务ID(DB_TRX_ID )7字节的回滚指针(DB_ROLL_PTR)隐藏的ID6字节的事物ID用来标识该行所述的事务,7字节的回滚指针需要了解下Innodb的事务模型。 1. Innodb的事务相关概念为了支持事务,Innbodb引入了下面几个概念:redo logredo log就是保存执行的SQL语句到一个指定的Log文件,当Mysql执行recovery时重新执行redo log记录的SQL操作即可。 当客户端执行每条SQL(更新语句)时,redo log会被首先写入log buffer;当客户端执行COMMIT命令时,log buffer中的内容会被视情况刷新到磁盘。 redo log在磁盘上作为一个独立的文件存在,即Innodb的log文件。 undo log与redo log相反,undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。 undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。 rollback segment回滚段这个概念来自Oracle的事物模型,在Innodb中,undo log被划分为多个段,具体某行的undo log就保存在某个段中,称为回滚段。 可以认为undo log和回滚段是同一意思。 锁Innodb提供了基于行的锁,如果行的数量非常大,则在高并发下锁的数量也可能会比较大,据Innodb文档说,Innodb对锁进行了空间有效优化,即使并发量高也不会导致内存耗尽。 对行的锁有分两种:排他锁、共享锁。 共享锁针对对,排他锁针对写,完全等同读写锁的概念。 如果某个事务在更新某行(排他锁),则其他事物无论是读还是写本行都必须等待;如果某个事物读某行(共享锁),则其他读的事物无需等待,而写事物则需等待。 通过共享锁,保证了多读之间的无等待性,但是锁的应用又依赖Mysql的事务隔离级别。 隔离级别隔离级别用来限制事务直接的交互程度,目前有几个工业标准:- READ_UNCOMMITTED:脏读- READ_COMMITTED:读提交- REPEATABLE_READ:重复读- SERIALIZABLE:串行化Innodb对四种类型都支持,脏读和串行化应用场景不多,读提交、重复读用的比较广泛,后面会介绍其实现方式。 2. 行的更新过程下面演示下事务对某行记录的更新过程:1. 初始数据行F1~F6是某行列的名字,1~6是其对应的数据。 后面三个隐含字段分别对应该行的事务号和回滚指针,假如这条数据是刚INSERT的,可以认为ID为1,其他两个字段为空。 2.事务1更改该行的各字段的值当事务1更改该行的值时,会进行如下操作:用排他锁锁定该行
发表评论