- 数据生态:MySQL复制技术与生产实践
- 罗小波
- 4687字
- 2025-06-24 12:09:10
第6章 多线程复制
MySQL 5.6之前的版本不支持从库并行重放主库的二进制日志,所以一旦主库的写压力稍微大一点,从库就容易出现延迟。当然,目前最新的MySQL版本已经能够很好地支持多线程复制。为了便于理解复制是如何一步一步演进为多线程复制的,本章将从单线程复制说起。在开始学习本章内容之前,也许你需要回顾一下复制的基本原理,详见第2章“复制的基本原理”。
提示:
• 本章中解析的所有二进制日志示例均为 row格式。
• 下文中提到的“单线程复制”也可称为“串行复制”,“多线程复制”也可称为“并行复制”(注意,这里所说的“串行复制”与“并行复制”,指的是数据库中数据变更操作的“串行”与“并行”,不要和复制拓扑中的主从复制的“串行”与“并行”混淆)。
• 下文中提到的系统变量详见《千金良方:MySQL性能优化金字塔法则》附录C“MySQL常用配置变量和状态变量详解”。
6.1 单线程复制原理
单线程复制是MySQL中最早出现的Server之间的数据同步技术,当从库的I/O线程将主库二进制日志写入自身的中继日志之后,读取中继日志并进行回放的线程只有一个,也就是SQL Thread(SQL线程),参见图6-1。

图6-1
下面以单线程复制中主库写入二进制日志的日志解析记录为例,对单线程复制原理进行详细的阐述。假设主库中执行了一个INSERT操作,那么在二进制日志中的记录如下(MySQL 5.5):

如图6-1所示,从库的SQL线程从中继日志中读取并解析主库二进制日志,由于执行事件的线程只有一个,所以读取的所有事件被串行执行。而二进制日志的写入时机是在事务发起提交之后,也就是说,主库事务先执行,然后产生的二进制日志才会被发送到从库执行。所以,从理论上讲,主从库这一前一后的时差就必然会导致从库复制延迟。如果遇到大事务,则从库延迟会急剧增加,例如,主库执行一个大事务耗费1小时,当从库收到这个事务之后开始执行时,就已经落后于主库1小时了。也正是因为单线程复制的效率极端低下,倒逼单线程复制向着多线程复制发展。
提示:前面提到,MySQL 5.6之前的版本,从库不支持多线程复制,但实际上在这之前的版本中,当不启用二进制日志时,InnoDB存储引擎本身是支持Group Commit的(即支持一次提交多个事务),但当启用二进制日志之后,为了保证数据的一致性(也就是必须保证MySQL Server层和存储引擎层的提交顺序一致),启用了两阶段提交。而两阶段提交中的prepare阶段使用了prepare_commit_mutex互斥锁来强制事务串行提交,这也大大降低了数据库的写入效率。
6.2 DATABASE多线程复制
6.2.1 原理
顾名思义,DATABASE多线程复制就是在单线程复制的基础之上做了改进,基于库级别的多线程复制。MySQL从5.6版本开始,支持库级别的多线程复制,这也是最早出现的多线程复制机制,在二进制日志中记录类似如下的内容(MySQL 5.6):

以上对于INSERT语句二进制日志的解析内容,与MySQL 5.5的解析内容相比,几乎没有什么变化。那么,从MySQL 5.6开始,对复制功能的改进主要是什么呢?
对于实例自身的事务而言(这里指的是本地事务,不区分主从库),在原先的两阶段提交中,移除了prepare_commit_mutex互斥锁。为保证二进制日志的提交顺序和存储引擎层的一致,引入与InnoDB存储引擎层的Group Commit类似的机制,并将其称为Binary Log Group Commit(BLGC)。在MySQL数据库上层提交事务时,首先按照顺序将事务放入一个队列中,队列中的第一个事务称为leader,其他事务称为follower,leader控制follower的行为。一旦前一个阶段中的队列任务执行完,后一个阶段队列中的leader就会带领它的follower进入前一个阶段的队列中执行,这样的并行提交可以持续不断地进行。BLGC的步骤分为图6-2所示的3个阶段(该图来自mysqlmusings blogspot)。

图6-2
(1)Flush阶段:将每个事务的二进制日志写入内存。
(2)Sync阶段:将内存中的二进制日志刷新到磁盘,若队列中有多个事务,那么仅一次fsync操作就完成多个事务的二进制日志写入,这就是BLGC。
(3)Commit阶段:leader根据顺序调用存储引擎层事务的提交,InnoDB存储引擎本就支持Group Commit,因此修复了原先由prepare_commit_mutex互斥锁导致InnoDB存储引擎层Group Commit失效的问题。这样一来,在启用二进制日志的情况下,就实现了数据库中事务的并行提交。
对于从库事务而言(这里指的是从库通过二进制日志重放的主库事务),主要的改进在于从库复制的SQL线程——增加了一个SQL协调器线程(Coordinator线程),真正干活的SQL线程被称为工作线程(Worker线程),当Worker线程为N个(N > 1)以及主库的DATABASE(Schema)为N个时,从库就可以根据多个DATABASE之间相互独立(彼此之间无锁冲突)的语句来实现多线程复制;反之,如果N = 1,则多线程复制跟MySQL 5.6之前版本中的单线程复制没有太大区别。多线程复制大致的工作流如图6-3所示。
提示:对于DATABASE多线程复制,如果有跨库事务,并行的Worker线程之间可能产生相互等待。

图6-3
6.2.2 系统变量的配置
1. 主库

2. 从库

6.3 LOGICAL_CLOCK多线程复制
6.3.1 原理
MySQL从5.7版本开始,支持LOGICAL_CLOCK多线程复制。基于MySQL 5.6库级别的Group Commit多线程复制做了大幅改进。对于DATABASE多线程复制,允许并行回放的粒度为数据库级别,只有在同一时间修改数据且修改操作针对的是不同数据库,才允许并行。而对于LOGICAL_CLOCK多线程复制,允许并行回放的粒度为事务级别,即便在同一时间修改数据的操作针对的是同一个数据库,理论上只要事务之间不存在锁冲突,就允许并行,可通过设置系统变量slave_parallel_type为LOGICAL_CLOCK来启用,如果该变量被设置为DATABASE,则与MySQL 5.6的多线程复制相同。从字面上无法直观地看出LOGICAL_CLOCK是基于什么维度来实现多线程复制的。下面我们通过解析二进制日志的内容来进行解读。

从以上对于INSERT语句的二进制日志解析内容来看,MySQL从5.7版本开始,新增了两个事件类型,Anonymous_gtid_log_event类型用于记录未启用GTID时的Binlog Group信息,Gtid_log_event类型用于记录启用GTID时的Binlog Group信息。利用这些信息,从库的SQL线程在应用主库的二进制日志时,就可以并行回放,大大提高了从库复制的效率。
从上述代码段的注解中我们可以知道,拥有相同last_committed值的事务可以并行回放,但是事务的last_committed值是如何确定的呢?
last_committed值是主库事务在进入prepare阶段时获取的已提交事务的最大sequence_number值。这个值称为此事务的commit-parent,被记录在二进制日志中,当从库回放事务时,如果两个事务有同一个commit-parent,它们就可以并行执行。
提示:
• 在LOGICAL_CLOCK多线程复制中,发生变更的是主库记录二进制日志的算法与格式,以及从库分发事务的算法。至于从库的复制线程,仍然沿用图6-3所示的框架。
• 虽然这种方式大幅提高了从库复制的效率,也可以说,允许并行回放的粒度细化到事务级别,甚至可以说细化到行级别(每个事务只修改一行数据)。但是,可以并行回放的事务必须具有相同的last_committed值,即使两个事务的数据完全不相关,但如果last_committed值不同,也不能并行回放,而有多少事务具有相同的last_committed值,则由主库瞬时并发请求的数量而定(系统变量binlog_group_commit_sync_no_delay_count = 0时)。如果主库没有什么写压力,写入二进制日志中的每个事务的last_committed值都不相同,这时从库的复制实际上仍然是单线程复制。所以,LOGICAL_CLOCK多线程复制仍然有一定的优化空间。
• 在MySQL 5.7较新的版本中,将允许并行回放的算法升级为基于Lock(锁)的,即通过一个锁的时间范围来确定是否可以并行回放。主库会力求为每一个事务生成尽可能小的last_committed值,这样就可以提高从库回放的并行度,因为从库进行回放时判断是否可以并行回放的依据,就是last_committed值是否相同。该锁的时间范围设定为:以事务在两阶段提交的prepare阶段获取最后一把锁的时间为开始时间点,以commit阶段释放第一把锁的时间为结束时间点。如果锁的时间范围有重叠,事务就可以并行回放,无重叠就不能并行回放。在升级为“基于Lock”的多线程复制之前,主库为事务生成的last_committed值是在每个事务执行提交操作时获取的当前已提交完成事务的最大sequence_number,这种机制称为Commit-Parent-Based。对于该机制有兴趣的读者可自行研究,这里不展开阐述。
6.3.2 系统变量的配置
1. 主库

2. 从库

6.4 WRITESET多线程复制
6.4.1 原理
WRITESET多线程复制,其实是在MySQL 5.7的大于或等于5.7.22的版本、MySQL 8.0的大于或等于8.0.1的版本中,对LOGICAL_CLOCK多线程复制的优化机制。优化之后,只要不同事务的修改记录不重叠,就可以在从库中并行回放。通过计算每行记录的哈希值(hash)来确定其是否为相同的记录。该哈希值就是WRITESET值。
WRITESET多线程复制允许并行回放的粒度依然为事务级别。严格来说,WRITESET以及下文中提到WRITESET_SESSION都只是在原有的LOGICAL_CLOCK多线程复制的基础上优化事务并行度的依赖模式,默认的依赖模式为COMMIT_ORDER。但由于WRITESET和WRITESET_SESSION依赖模式在某些场景下,能够显著提高多线程复制的效率,加上这两种依赖模式都是基于全新引入的冲突检测数据库的机制实现的,因此,通常大家都习惯将它们一起称为“WRITESET多线程复制”,以便和COMMIT_ORDER依赖模式的“LOGICAL_CLOCK多线程复制”区分。
WRITESET多线程复制的本质就是基于WRITESET的值对生成last_committed值的依赖模式做了大量优化。下面我们通过解析二进制日志中记录的内容来进行解读。

从以上对于INSERT语句的二进制日志解析内容来看,WRITESET多线程复制与LOGICAL_CLOCK多线程复制记录的二进制日志相比,格式上没有什么变化,那么它对于复制的改进主要是什么呢?细心的读者可能已经发现,last_committed = 2的两个事务的时间戳并不是同一个时刻的,并且这两个事务之间,还夹了一个last_committed = 8的事务。在以往的LOGICAL_CLOCK多线程复制中几乎不可能出现这种情况,这其中发生了什么?
对于主库来说,WRITESET多线程复制对LOGICAL_CLOCK多线程复制的优化,并不是在二进制日志记录的格式上,而是在事务写二进制日志时对last_committed值的计算做了大量优化。在6.3.1节中,我们提到过,LOGICAL_CLOCK多线程复制的并行粒度为事务级别,在理论上虽然只要不同事务之间不存在锁冲突即可并行,但实际上采用默认的COMMIT_ORDER依赖模式的多线程复制,在从库中的并行分发机制上并不能完全实现。这是因为在默认的COMMIT_ORDER依赖模式下,主库生成二进制日志时,只有同一时间提交的事务生成的last_committed值才相同,但实际上不同时间提交的事务也有不存在锁冲突的可能。因此,这些事务在主库中生成二进制日志时的last_committed值理论上也应该相同。要实现在主库生成中生成二进制日志时,在不同时间提交的且不存在锁冲突的事务生成相同的last_committed值,必须引入新机制,而这个新的机制,便是WRITESET多线程复制对LOGICAL_CLOCK多线程复制优化的关键。
通过唯一索引或主键索引来区分不同的记录,然后和行记录的库表属性以及数据属性一起计算哈希值,计算出的WRITESET值存放在一张哈希表中。后面如果新事务的行记录计算出的哈希值在哈希表中无匹配记录,那么此新事务不会产生新的last_committed值,就相当于新事务和之前的事务被归并到同一个Binlog Group,即新旧事务的last_committed值相同。如果新事务的行记录计算出的哈希值在哈希表中找到了匹配记录,则表示存在事务冲突,就会产生新的last_committed值(即产生了一个新的Binlog Group)。具体的计算公式如下:
WRITESET = hash(index_name,db_name,db_name_length,table_name,table_name_length, value,value_length)
主库中生成last_committed值的依赖模式改进如图6-4所示。

图6-4
根据系统变量binlog_transaction_dependency_tracking的设置,采用不同的依赖模式生成last_committed值。
在WRITESET多线程复制中做冲突认证的过程如图6-5所示。
对于从库来说,并行应用二进制日志的逻辑几乎没有变化,仍然根据last_committed的值判断是否可以并行回放。
提示:
WRITESET多线程复制在如下场景中不可用:
• DDL语句。
• 会话当前生效的哈希算法(可以使用系统变量transaction_write_set_extraction在会话级别修改)和生成writeset_history列表值使用的哈希算法不同(哈希算法被动态修改过之后,碰到不同算法产生的值将无法进行比较)。
• 事务更新了被外键关联的字段。
使用WRITESET多线程复制时,如果主库中具有高并发的事务且这些事务具备“短、平、快”的特性,则WRITESET多线程复制中的WRITESET、WRITESET_SESSION依赖模式与默认的COMMIT_ORDER依赖模式相比,对从库的多线程复制效率并没有太大提高,但如果主库中有高并发的事务且这些事务中大事务居多,则使用COMMIT_ORDER依赖模式时,二进制日志中的last_committed值重复率可能不够高,导致从库多线程复制的效率大大降低。在这种情况下,使用WRITESET多线程复制中的WRITESET、WRITESET_SESSION依赖模式能够大大提高主库二进制日志中last_committed值的重复率,大幅提高从库的多线程复制效率。即便如此,也不建议在MySQL中写入大事务,过大的事务在从库的协调器线程做事务分发时极易成为瓶颈,进而使多线程复制的效率难以提高,而且大事务还会带来诸多负面影响。

图6-5
生成last_committed值的部分代码如下(仅供参考,有兴趣的读者请自行翻阅源码文件sql/rpl_trx_tracking.cc,这里不再赘述):





6.4.2 系统变量的配置
1. 主库

2. 从库

提示:更多信息可参考高鹏的“复制”专栏,登录简书网站搜索“第16节:基于WRITESET的并行复制方式”。