如何处理事务?MySQL并行复制(MTS)原理解读
❝在MySQL 5.7版本,官方称为enhanced multi-threaded slave(简称MTS),就是:master基于组提交(group commit)来实现的并发事务分组,再由slave通过SQL thread将一个组提交内的事务分发到各worker线程,实现并行应用。
MySQL 5.7并行复制原理
MySQL 5.6基于库的并行复制出来后,基本无人问津,MySQL 5.7才可称为真正的并行复制,这其中最为主要的原因就是slave服务器的回放与master是一致的,即master服务器上是怎么并行执行的,那么slave上就怎样进行并行回放。不再有库的并行复制限制,对于二进制日志格式也无特殊的要求(基于库的并行复制也没有要求)。
从MySQL官方来看,其并行复制的原本计划是支持表级的并行复制和行级的并行复制,行级的并行复制通过解析ROW格式的二进制日志的方式来完成。
该并行复制的思想最早是由MariaDB的Kristain提出,并已在MariaDB 10中出现,相信很多选择MariaDB的小伙伴最为看重的功能之一就是并行复制。MTS实现了事务的并行,从某种程度来说也实现了行的并行(事务对行处理)。
下面来看看MySQL 5.7中的并行复制究竟是如何实现的?
order commit (group commit) -> logical clock ->> MTS
Master
组提交(group commit)
组提交(group commit):通过对事务进行分组,优化减少了生成二进制日志所需的操作数。当事务同时提交时,它们将在单个操作中写入到二进制日志中。如果事务能同时提交成功,那么它们就不会共享任何锁,这意味着它们没有冲突,因此可以在Slave上并行执行。所以通过在主机上的二进制日志中添加组提交信息,这些Slave可以并行地安全地运行事务。
首先,MySQL 5.7的并行复制基于一个前提,即所有已经处于prepare阶段的事务,都是可以并行提交的。这些当然也可以在从库中并行提交,因为处理这个阶段的事务,都是没有冲突的,该获取的资源都已经获取了。反过来说,如果有冲突,则后来的会等已经获取资源的事务完成之后才能继续,故而不会进入prepare阶段。这是一种新的并行复制思路,完全摆脱了原来一直致力于为了防止冲突而做的分发算法,等待策略等复杂的而又效率底下的工作。
MySQL 5.7并行复制的思想一言以蔽之:一个组提交(group commit)的事务都是可以并行回放,因为这些事务都已进入到事务的prepare阶段,则说明事务之间没有任何冲突(否则就不可能提交)。
根据以上描述,这里的重点是:
- 如何来定义哪些事务是处于prepare阶段的?
- 在生成的Binlog内容中该如何告诉Slave哪些事务是可以并行复制的?为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量slave-parallel-type,其可以配置的值有:DATABASE(默认值,基于库的并行复制方式),LOGICAL_CLOCK(基于组提交的并行复制方式)
支持并行复制的GTID
那么如何知道事务是否在同一组中?原版的MySQL并没有提供这样的信息。在MySQL 5.7版本中,其设计方式是将组提交的信息存放在GTID中。
那么如果参数gtid_mode设置为OFF,用户没有开启GTID功能呢?MySQL 5.7又引入了称之为Anonymous_Gtid(ANONYMOUS_GTID_LOG_EVENT)的二进制日志event类型,
如:
mysql> SHOW BINLOG EVENTS in 'mysql-bin.000006';
+------------------+-----+----------------+-----------+-------------+-----------------------------------------------+
| Log_name | Pos | Event_type | Server_id | End_log_pos | Info |
+------------------+-----+----------------+-----------+-------------+-----------------------------------------------+
| mysql-bin.000006 | 4 | Format_desc | 88 | 123 | Server ver: 5.7.7-rc-debug-log, Binlog ver: 4|
| mysql-bin.000006 | 123 | Previous_gtids | 88 | 194 | |
| mysql-bin.000006 | 194 | Anonymous_Gtid | 88 | 259 | SET @@SESSION.GTID_NEXT= 'ANONYMOUS' |
| mysql-bin.000006 | 259 | Query | 88 | 330 | BEGIN |
| mysql-bin.000006 | 330 | Table_map | 88 | 373 | table_id: 108 (aaa.t) |
| mysql-bin.000006 | 373 | Write_rows | 88 | 413 | table_id: 108 flags: STMT_END_F |
......
这意味着在MySQL 5.7版本中即使不开启GTID,每个事务开始前也是会存在一个Anonymous_Gtid,而这个Anonymous_Gtid事件中就存在着组提交的信息。反之,如果开启了GTID后,就不会存在这个Anonymous_Gtid了,从而组提交信息就记录在非匿名GTID事件中。
- PREVIOUS_GTIDS_LOG_EVENT用于表示上一个binlog最后一个gitd的位置,每个binlog只有一个,当没有开启GTID时此事件为空。
- GTID_LOG_EVENT
- 当开启GTID时,每一个操作语句(DML/DDL)执行前就会添加一个GTID事件,记录当前全局事务ID。
- 同时在MySQL 5.7版本中,组提交信息也存放在GTID事件中,有两个关键字段last_committed,sequence_number就是用来标识组提交信息的。
- 在InnoDB中有一个全局计数器(global counter),在每一次存储引擎提交之前,计数器值就会增加。在事务进入prepare阶段之前,全局计数器的当前值会被储存在事务中,这个值称为此事务的commit-parent(也就是last_committed)。
slave
LOGICAL_CLOCK(由order commit实现),实现的group commit目的
然而,通过上述的SHOW BINLOG EVENTS,我们并没有发现有关组提交的任何信息。但是通过mysqlbinlog工具,就能发现组提交的内部信息。
$ mysqlbinlog mysql-bin.0000006 | grep last_committed#150520 14:23:11 server id 88 end_log_pos 259 CRC32 0x4ead9ad6 GTID last_committed=0 sequence_number=1#150520 14:23:11 server id 88 end_log_pos 1483 CRC32 0xdf94bc85 GTID last_committed=0 sequence_number=2#150520 14:23:11 server id 88 end_log_pos 2708 CRC32 0x0914697b GTID last_committed=0 sequence_number=3#150520 14:23:11 server id 88 end_log_pos 3934 CRC32 0xd9cb4a43 GTID last_committed=0 sequence_number=4#150520 14:23:11 server id 88 end_log_pos 5159 CRC32 0x06a6f531 GTID last_committed=0 sequence_number=5#150520 14:23:11 server id 88 end_log_pos 6386 CRC32 0xd6cae930 GTID last_committed=0 sequence_number=6#150520 14:23:11 server id 88 end_log_pos 7610 CRC32 0xa1ea531c GTID last_committed=6 sequence_number=7#150520 14:23:11 server id 88 end_log_pos 8834 CRC32 0x96864e6b GTID last_committed=6 sequence_number=8#150520 14:23:11 server id 88 end_log_pos 10057 CRC32 0x2de1ae55 GTID last_committed=6 sequence_number=9#150520 14:23:11 server id 88 end_log_pos 11280 CRC32 0x5eb13091 GTID last_committed=6 sequence_number=10#150520 14:23:11 server id 88 end_log_pos 12504 CRC32 0x16721011 GTID last_committed=6 sequence_number=11#150520 14:23:11 server id 88 end_log_pos 13727 CRC32 0xe2210ab6 GTID last_committed=6 sequence_number=12#150520 14:23:11 server id 88 end_log_pos 14952 CRC32 0xf41181d3 GTID last_committed=12 sequence_number=13...
上述的last_committed和sequence_number代表的就是所谓的LOGICAL_CLOCK。
可以发现MySQL 5.7二进制日志较之原来的二进制日志内容多了last_committed和sequence_number。
- last_committed表示事务提交时上次事务提交的编号,事务在进入prepare阶段时会将上次事务的sequence_number记录为自己的last_committed,如果事务具有相同的last_committed,表示这些事务都在一组内,可以进行并行的回放。
- 例如上述last_committed为0的事务有6个,表示组提交时提交了6个事务,而这6个事务在slave是可以进行并行回放的。
- 而sequence_number是顺序增长的,每个事务对应一个序列号,当事务完成committed时便会得到这个sequence_number。
另外,还有一个细节,下一个事务组的last_committed和上一个事务的sequence_number是相等的。这也很容易理解,因为事物是顺序提交的,这么理解起来并不奇怪。本组的 sequence_number最小值肯定大于last_committed。(这一块描述不严谨,在5.7后续版本中,官方优化了slave进行并行apply的规则,但是这里为了便于理解,不做修改,理解这个思路后阅读后面基于锁的并行规则也很容易。)
这两个值的有效作用域都在文件内,只要换一个binlog文件(flush binary logs),这两个值就都会从0开始计数。
MySQL是如何做到将这些事务分组的?
还有一个重要的技术问题:MySQL是如何做到将这些事务分组的?
要搞清楚这个问题,首先需要了解一下MySQL事务提交方式。
1. 事务两阶段提交
事务的提交主要分为两个主要步骤:
(不好理解这段就看上面的图解好了。)
2. Order Commit:是LOGICAL_CLOCK并行复制的基础
关于MySQL是如何提交的,内部使用ordered_commit函数来处理的。先看它的逻辑图,如下:
从图中可以看到,只要事务提交(调用ordered_commit),就都会先加入队列中。
提交有三个步骤,包括FLUSH、SYNC及COMMIT,相应地也有三个队列。
- 首先要加入的是FLUSH队列:
- 只要队长将这个队列中的事务取出,其他事务就可以加入这个等待队列了。第一个加入的还是队长,但此时必须要等待。因为此时有事务正在做FLUSH,做完FLUSH之后,其他的队长才能带着队员做FLUSH。
- 在同一时刻,只能有一个组在做FLUSH。这就是上图中所示的等待事务组2和等待事务组3,此时队长会按照顺序依次做FLUSH。
- 做FLUSH的过程中,有一些重要的事务需要去做,如下:
- 组内每个事务做finish_commit是在队长完成COMMIT工序之后进行,到步骤DONE时,便会唤醒每个等待提交完成的事务,告诉他们可以继续了,那么每个事务就会去做finish_commit。
t2-2,last_committed=4, sequence_number=5t4-1,last_committed=4, sequence_number=6t7-1,last_committed=4, sequence_number=7t8-1,last_committed=4, sequence_number=8
t3-2,last_committed=8, sequence_number=9t8-2,last_committed=8, sequence_number=10t9-1,last_committed=8, sequence_number=11
t1-2,last_committed=11, sequence_number=12t2-3,last_committed=11, sequence_number=13t6-1,last_committed=11, sequence_number=14t7-2,last_committed=11, sequence_number=15t8-3,last_committed=11, sequence_number=16
t1-3,last_committed=16, sequence_number=17t2-4,last_committed=16, sequence_number=18t4-2,last_committed=16, sequence_number=19t5-2,last_committed=16, sequence_number=20t8-4,last_committed=16, sequence_number=21
- 因为T4a时间点进行组提交后,delay 1000(5格时间单位)的提交时间点刚好在t10-1事务提交发生的同一时间。
t3-3,last_committed=21, sequence_number=22t6-2,last_committed=21, sequence_number=23t7-3,last_committed=21, sequence_number=24t9-2,last_committed=21, sequence_number=25t10-1,last_committed=21, sequence_number=26
- 如果拿出事务的last_committed(26)大于等于当前已经执行的sequence_number的最小值(22),说明拿出的事务是新的一组,拿出的事务需等待。
- Trx4的P对应的commit-parent(last_commited)取自所有已经执行完的事务的最大的C对应的sequence_number=1,也就是Trx1的C对应的sequence_number。因为这个时候Trx1已经执行完,但是Trx2还未执行完。
- Trx5 和Trx6可以并发执行,因为他们的commit-parent是相同的,都是由Trx2设定的。
所以官方对并行复制的机制做了改进,提出了一种新的并行复制的方式:Lock-Based Scheme。# 5.7开始基于lock interval的并行规则(WL#7165)
- 在基于last_committed规则下,Trx4、Trx5和Trx6在同一时间持有各自的锁,但Trx4无法并发执行,因为Trx4取到的laste_committed和后两者不同。
- 对于L(lock interval的开始点),MySQL会把global.max_committed_timestamp分配给一个变量,并取名叫transaction.last_committed。
- 对于C(lock interval的结束点),MySQL会给每个事务分配一个逻辑时间戳(logical timestamp),命名为:transaction.sequence_number。
t3,last_committed=3, sequence_number=5t4,last_committed=3, sequence_number=6t5,last_committed=3, sequence_number=7
last_committed=6 sequence_number=8 last_committed=6 sequence_number=9
t8,last_committed=9, sequence_number=10
