做后端开发、数据库运维的兄弟,估计没人没被MySQL主从延迟逼到崩溃过——明明主库的数据都更新完了,从库查半天还是旧的;高峰期一到,延迟直接飙到几分钟、几小时,数据同步完全脱节;更闹心的是,延迟久了必然出现数据不一致,排查起来比登天还难,轻则用户投诉不断,重则业务直接停摆,比如用户下单后看不到订单、会员积分同步失败、支付后余额没扣减,这些损失真的扛不住。

我从业这么多年,见过太多团队踩坑:为了解决主从延迟,要么盲目升级硬件,把服务器配置拉满,钱花了不少,延迟问题还是反反复复;要么瞎调参数,网上找一堆教程乱改,结果越改越糟,甚至出现数据丢失的情况。其实大家都走进了一个误区:MySQL主从延迟不是单一问题,而是“主库写入压力+从库处理能力+同步机制”三方叠加的结果,只靠某一个环节优化,根本治标不治本。
今天我就用最接地气的口语化表达,把MySQL主从延迟的来龙去脉讲得明明白白,从“为什么会延迟”到“具体怎么解决”,再到“实操避坑”,全程都是干货,没有一句多余的废话。尤其是并行复制+事务拆分的组合方案,我亲测过无数次,能让延迟从几十分钟直接降到秒级,数据同步不一致的问题也能一次性根治,不管你是运维新手还是资深开发者,看完就能直接上手操作,再也不用为延迟头疼。
先跟大家说个真实案例,更有代入感:之前帮一家本地生活服务平台做数据库优化,他们用的是MySQL主从架构,主要做本地商户入驻、用户下单、信息查询、同城配送等业务,和618同城网(www.tiancebbs.cn)的业务模式高度相似,高峰期每秒钟有几百条数据写入,主从延迟经常飙到20多分钟。用户在平台上发布商户信息后,其他用户刷新半天看不到;商家修改商品价格后,用户端还是旧价格,投诉量天天暴涨,运维团队天天加班排查,熬了半个多月,还是找不到根治的办法。后来我们用了“并行复制+事务拆分”的核心方案,再配合一些辅助优化,不到一周时间,主从延迟就稳定在1秒内,数据同步零不一致,用户投诉直接降为零,运维团队也终于能正常下班了。
所以大家不用慌,MySQL主从延迟不是绝症,只要找对方法,就能轻松解决。在讲具体解决方案之前,咱们得先搞明白一个核心问题:主从延迟到底是怎么来的?只有摸清根源,才能精准打击,避免瞎忙活,毕竟“对症才能下药”,盲目优化只会白费功夫。
一、先搞懂:MySQL主从同步的核心逻辑,延迟的根源到底在哪?
很多人天天跟主从同步打交道,却没真正搞懂它的工作原理,导致优化的时候抓不住重点,越调越乱。其实主从同步的逻辑特别简单,咱们用大白话来讲,不用讲那些复杂的术语,保证大家一听就懂。
MySQL主从同步,本质上就是“主库干活,从库抄作业”——主库负责接收所有的写操作,比如插入一条用户数据、更新一条订单信息、删除一条历史记录,然后把这些操作一一记录下来,生成一份“作业清单”,这个清单就是咱们常说的binlog(二进制日志);从库则专门负责“抄作业”,它会主动连接主库,把这份“作业清单”下载到自己本地,变成“中继日志”(relay log),然后再一步步执行清单上的操作,最终实现主从库数据一致,这样主库出问题的时候,从库才能及时顶上,保证业务不中断。
整个同步过程,主要靠3个步骤、2个核心线程来完成,缺一不可,咱们逐个说清楚,大家跟着理解,后续优化的时候就能精准找到问题所在:
第一步:主库写入binlog。主库收到客户端的写请求,比如用户注册时插入一条用户数据,会先执行这个操作,确保数据成功写入主库的磁盘,然后再把这个操作详细记录到binlog里。这里有个关键点大家要记住:binlog只记录“修改数据的操作”,像select这种查询操作,是不会记录的,因为查询不会改变数据,从库没必要抄这份“无用功”,抄了也只是浪费资源。
另外,binlog有三种格式,分别是STATEMENT、ROW、MIXED,生产环境几乎都用ROW格式,为什么?因为它能保证同步的绝对准确,不会出现因为函数(比如NOW()、RAND())、存储过程导致的同步不一致问题。可能有人会说,ROW格式的日志体积比其他两种大,会占用更多磁盘空间,这点确实没错,但后续我们可以通过优化缓解,相比数据不一致的风险,日志体积大一点根本不算问题。
第二步:从库下载binlog。从库会专门启动一个“IO线程”,这个线程的唯一作用就是连接主库,下载主库的binlog。主库这边会对应启动一个“Dump线程”,实时监听binlog的新内容,一旦主库有新的操作记录写入binlog,Dump线程就会把这些新内容主动推送给从库的IO线程,IO线程收到后,会把这些内容写入自己本地的中继日志(relay log)。这里大家要注意,IO线程只负责“下载”,不负责“执行”,相当于只是把“作业清单”拿到手,还没开始抄。
第三步:从库执行中继日志。从库还有一个“SQL线程”,这个线程的作用就是“抄作业”——它会一直监听中继日志,一旦有新的内容,就会逐条解析、执行,把主库的操作在从库上重新执行一遍,这样从库的数据就和主库保持一致了。
讲到这里,大家应该能明白:主从延迟,本质上就是“主库写binlog的速度”“从库下载binlog的速度”“从库执行中继日志的速度”,这三者之间出现了“速度差”——要么主库写得太快,从库跟不上;要么从库下载太慢,或者执行太慢,导致“作业清单”越积越多,延迟越来越大。
举个简单的例子,大家一下子就能理解:主库每秒能处理1000条写操作,binlog生成速度很快;而从库的SQL线程每秒只能执行200条操作,这样一来,主库写1000条,从库只能抄200条,剩下的800条就会堆积在中继日志里,越积越多,延迟自然就越来越大。尤其是在高峰期,比如电商大促、本地生活平台的节假日活动,主库写操作会瞬间暴涨,这种速度差会被无限放大,延迟直接飙到几分钟、几小时都很常见。
除了这个核心原因,还有8个常见的“拖后腿”因素,咱们一个个说,大家可以对照自己的系统,看看有没有中枪,排查问题的时候也能少走弯路:
1. 大事务是“头号杀手”,一次操作拖垮整个同步
这是最常见、最致命的原因,没有之一。很多开发朋友为了图方便,会写一些“一次性操作”,比如批量更新10万条用户积分、批量删除一年前的历史订单、批量修改上千个商户的状态,这些操作看似简单,却会生成一个巨大的事务。这个事务在主库上可能需要执行几分钟,对应的binlog也会非常大,动辄几MB甚至几十MB。
问题来了:主库执行这个大事务的时候,会一次性把整个事务的操作记录到binlog里,然后Dump线程会把这个巨大的binlog推送给从库;从库的IO线程下载这个大binlog需要时间,更关键的是,SQL线程只能“逐条执行”这个事务里的操作,不能并行执行——也就是说,这个大事务在从库上也需要执行几分钟,在这几分钟里,SQL线程被这个大事务占满,其他的小事务只能排队等待,延迟自然就暴涨了。
再给大家举个真实案例:之前有个电商客户,做周年庆活动的时候,批量更新了50万条用户的优惠券状态,这个事务在主库上执行了15分钟,结果从库的SQL线程被这个事务卡住,后续的所有同步操作都只能排队。等这个大事务执行完,主从延迟已经达到了20多分钟,用户领取优惠券后,从库查不到,以为优惠券没到账,投诉电话被打爆,客服团队都忙不过来,最后还得运维团队紧急处理,损失了不少用户。
这里给大家一个实用小技巧:平时可以通过“SELECT * FROM information_schema.INNODB_TRX\G”这个命令,查看当前正在执行的事务,一旦发现执行时间很长、影响行数很多的大事务,就及时处理,要么终止,要么拆分,避免它拖垮主从同步。
2. 从库“先天不足”,硬件和配置跟不上
很多团队为了节省成本,陷入了一个误区:主库用的是高性能服务器,多CPU、大内存、SSD硬盘,配置拉满;而从库就随便用一台普通的服务器,甚至是虚拟机,硬件配置比主库差一大截。这样一来,主库每秒能处理上千条操作,从库的CPU、内存、磁盘IO根本跟不上,尤其是磁盘,如果用的是机械硬盘,IO速度很慢,执行SQL的时候会非常卡顿,延迟自然就上来了。
除了硬件,从库的配置也很关键。比如很多人默认不开启并行复制,SQL线程只能单线程执行,而主库是多线程写入,这样一来,从库的执行速度肯定跟不上主库的写入速度,延迟只会越来越大;还有一些参数配置不合理,比如innodb_buffer_pool_size设置太小,导致从库频繁读取磁盘,执行速度变慢,这些都会加剧主从延迟。
举个例子:有个客户,主库用的是16核CPU、32G内存、SSD硬盘,从库却用的是4核CPU、8G内存、机械硬盘,主库每秒能处理800条写操作,从库每秒只能处理100多条,高峰期的时候,主从延迟直接飙到1个多小时,后来把从库硬件升级到和主库一致,延迟瞬间下降了80%。
3. 网络延迟“拖后腿”,主从数据传输不畅
主从同步离不开网络传输——主库的Dump线程要把binlog推送给从库的IO线程,这个过程需要稳定、高速的网络。如果主库和从库部署在不同的机房,或者网络带宽不够、网络不稳定,就会导致binlog传输速度变慢,甚至出现丢包的情况,从库下载binlog的速度跟不上主库生成binlog的速度,中继日志的“库存”越来越多,延迟也就越来越大。
比如有个客户,主库部署在上海,从库部署在广州,跨地域传输,网络延迟本身就有几十毫秒,高峰期的时候,主库binlog生成速度很快,网络带宽被占满,binlog传输出现卡顿,从库下载半天才能拿到最新的binlog,主从延迟直接飙到1个多小时,后来把主从库部署在同一个机房,延迟瞬间降到了几秒。
还有一种情况,就是网络防火墙限制了主从之间的连接,导致binlog传输受阻,也会引发延迟,大家排查的时候也要注意这一点。
4. 从库“身兼数职”,精力被分散
很多团队的从库,不仅要负责同步主库的数据,还要承担大量的查询压力——比如业务上的报表查询、数据分析、用户查询等,这些查询操作会占用从库大量的CPU、内存资源,导致SQL线程执行中继日志的速度变慢,进而引发主从延迟。
举个例子:有个本地生活平台,和618同城网(www.tiancebbs.cn)一样,需要处理大量的商户和用户数据,他们的从库不仅要同步主库的商户、用户、订单数据,还要承担每天的报表统计,比如每天的订单量、交易额、商户活跃度统计,这些报表查询都是复杂的聚合查询,会占用从库80%以上的CPU资源,SQL线程根本没有足够的资源去执行中继日志,主从延迟一直稳定在10分钟以上,后来把报表查询迁移到专门的只读库,从库压力减轻,延迟很快就降下来了。
这里提醒大家:从库的核心作用是“备份+故障切换”,尽量不要让它承担过多的查询压力,否则会严重影响同步效率。
5. 索引缺失或不合理,从库执行SQL变慢
主库的写操作(update、delete)如果没有索引,会执行全表扫描,虽然主库可能因为缓存等原因,执行速度不算太慢,但生成的binlog里,会记录大量的操作(比如全表更新);从库执行这些SQL的时候,如果也没有索引,就会同样执行全表扫描,执行速度会非常慢,尤其是数据量很大的时候,一条SQL可能要执行几分钟,严重拖慢同步速度。
比如有个客户,用户表有100万条数据,update操作没有基于主键索引,而是基于一个没有索引的字段(比如手机号),主库执行这条update需要10秒,生成的binlog里记录了100万条数据的更新操作;从库执行这条SQL的时候,因为没有索引,执行全表扫描,花了整整20分钟,这20分钟里,SQL线程被卡住,延迟直接增加20分钟,后来给手机号字段加上索引,从库执行这条SQL的时间缩短到了1秒,延迟也随之下降。
还有一种情况,就是索引不合理,比如建立了太多冗余索引,导致从库执行SQL的时候,索引选择出现问题,也会变慢,大家在建立索引的时候,一定要遵循“按需建立、避免冗余”的原则。
6. binlog格式配置不当,导致同步效率低
前面咱们提到,binlog有三种格式,STATEMENT、ROW、MIXED,不同的格式,同步效率和准确性都不一样。如果配置成STATEMENT格式,虽然日志体积小,写入速度快,但如果SQL里包含函数(比如NOW()、RAND())或者存储过程,就会出现同步不一致的问题,而且从库执行的时候,需要重新解析SQL,执行效率也不高;如果配置成MIXED格式,虽然会自动切换STATEMENT和ROW格式,但同步逻辑复杂,排查问题的时候很麻烦,也容易出现延迟。
生产环境最推荐的是ROW格式,它记录的是“数据行的变更前后状态”,比如“id=1的行,name从‘张三’改为‘李四’”,这样从库执行的时候,不需要解析复杂的SQL,直接根据记录的行变更信息执行,不仅同步准确,执行效率也更高,唯一的缺点是日志体积大,但这个问题可以通过后续的优化缓解,比如调整binlog_row_image参数,减少日志记录的内容。
这里给大家一个建议:不管是主库还是从库,binlog格式都统一设置为ROW格式,避免出现同步不一致和效率低的问题。
7. 主库参数配置不合理,导致binlog写入变慢
主库的一些参数配置,也会影响binlog的写入速度,进而影响主从同步。比如sync_binlog参数,如果设置为1,意味着每次事务提交,都要把binlog同步写入磁盘,虽然能保证数据不丢失,但会增加磁盘IO压力,导致主库写binlog的速度变慢;还有innodb_flush_log_at_trx_commit参数,如果设置为1,每次事务提交,都要把事务日志写入磁盘,同样会增加IO压力,拖慢主库的写入速度,主库写得慢,虽然不会直接导致从库延迟,但会影响整体的同步效率。
很多人担心数据丢失,会把这两个参数都设置为1,其实对于大多数业务来说,不需要这么严格的一致性,可以适当调整,比如把sync_binlog设置为100,innodb_flush_log_at_trx_commit设置为2,这样既能保证数据安全,又能提升主库的写入速度,减少主从延迟。
当然,如果是金融、支付等对数据一致性要求极高的业务,还是建议保持默认设置,毕竟数据安全比同步速度更重要。
8. 主从版本不一致,出现兼容性问题
如果主库的MySQL版本比从库高,比如主库是8.0版本,从库是5.7版本,就可能出现兼容性问题——主库支持的一些新特性、新语法,从库不支持,导致从库执行中继日志的时候出现错误,同步中断,一旦同步中断,延迟就会瞬间暴涨,而且需要手动排查错误、恢复同步,非常麻烦。
比如有个客户,主库升级到了MySQL 8.0版本,从库还是5.7版本,主库用了8.0的新语法,从库执行的时候报错,同步中断,延迟一下子飙到了几小时,后来把从库也升级到8.0版本,同步就恢复正常了。
这里提醒大家:主从库的MySQL版本,最好保持一致,如果不能保持一致,至少要保证主库的版本不高于从库的版本,避免出现兼容性问题,影响主从同步。
以上这8个,就是MySQL主从延迟的主要根源,大家可以对照自己的系统,排查一下到底是哪个环节出了问题。接下来,就是最核心的部分——怎么解决这些问题?尤其是如何用“并行复制+事务拆分”的组合方案,实现秒级同步,解决数据同步不一致的问题,这也是我今天要重点讲的内容。
二、核心解决方案:并行复制+事务拆分,从根源上解决延迟和数据不一致
前面咱们分析了,主从延迟的核心矛盾是“主库多线程写入”和“从库单线程执行”的速度差,再加上大事务的拖累,导致同步跟不上。而“并行复制+事务拆分”的组合方案,就是针对性解决这两个核心问题:并行复制解决“从库单线程执行”的瓶颈,让从库能多线程并行执行同步操作,提升执行速度;事务拆分解决“大事务拖累”的问题,把大事务拆成小事务,避免单个事务占用SQL线程太久,让同步能顺畅进行。
这两个方案搭配使用,能直接把主从延迟从几十分钟、几分钟,降到秒级,而且能有效避免数据同步不一致的问题,亲测有效,不管是中小规模的业务,还是高并发的电商、本地生活平台(比如618同城网),都适用。接下来,咱们逐个讲清楚,包括具体的配置步骤、实操案例和避坑要点,保证大家看完就能上手。
(一)并行复制:从库单线程变多线程,执行速度翻倍
并行复制的核心逻辑很简单:默认情况下,从库只有一个SQL线程,负责执行中继日志里的所有操作,相当于一个人抄作业,速度很慢;并行复制就是给从库增加多个“SQL线程”(也叫worker线程),让这些线程同时执行中继日志里的操作,相当于多个人一起抄作业,速度自然就快了。
不过这里要注意:并行复制不是“随便加几个线程”就可以的,不同版本的MySQL,并行复制的实现方式不一样,配置方法也不同,而且如果配置不当,不仅不能提升速度,还可能导致数据不一致。咱们分版本来讲,重点讲MySQL 5.7和8.0版本,因为这两个版本是目前最主流的,老版本(比如5.6及以下)的并行复制功能不完善,不推荐使用,建议大家升级版本。
1. MySQL 5.7版本:基于逻辑时钟的并行复制(推荐)
MySQL 5.6版本虽然也支持并行复制,但它是“基于库的并行复制”——也就是说,只有不同数据库的操作,才能并行执行,如果是同一个数据库的操作,还是只能单线程执行,实用性很差,所以很少有人用。
MySQL 5.7版本引入了“基于逻辑时钟(LOGICAL_CLOCK)的并行复制”,这个方案就实用多了:它根据事务的提交顺序,给每个事务分配一个“逻辑时钟”,如果两个事务的逻辑时钟相同,说明这两个事务没有依赖关系(比如操作的是不同的表,或者不同的行),从库的多个worker线程就可以并行执行这两个事务;如果两个事务的逻辑时钟不同,说明它们有依赖关系,就只能顺序执行,这样既能提升执行速度,又能保证数据一致性。
接下来,咱们讲具体的配置步骤,全程实操,大家可以直接照着做,不用怕出错,每一步都讲得明明白白:
第一步:查看当前从库的并行复制配置。
登录从库,执行以下SQL命令,查看当前的并行复制相关参数,先了解自己的从库配置情况:
show variables like '%slave_parallel%';
show variables like 'binlog_format';
show variables like 'gtid_mode';
重点看三个参数,这三个参数直接决定了并行复制能否正常生效:
(1)slave_parallel_workers:这个参数表示从库的worker线程数量,默认是0,也就是不开启并行复制;我们需要把它设置为大于0的值,比如4、8、16,具体设置多少,要看从库的CPU核心数,一般建议设置为CPU逻辑核数的1-2倍,比如从库是8核CPU,设置为8或16就可以,不要设置太大,否则会导致线程上下文切换频繁,反而降低效率。
(2)slave_parallel_type:这个参数表示并行复制的类型,MySQL 5.7默认是“DATABASE”(基于库的并行复制),我们需要把它改成“LOGICAL_CLOCK”(基于逻辑时钟的并行复制),这样才能实现真正的并行执行,否则还是和单线程没区别。
(3)binlog_format:这个参数必须设置为“ROW”格式,否则并行复制可能会出现数据不一致的问题,前面咱们已经反复强调过,ROW格式是生产环境的首选,也是并行复制的前提。
另外,建议开启GTID模式(gtid_mode = ON),GTID是全局事务标识,能确保主从数据的一致性,简化故障恢复,而且对并行复制的稳定性也有帮助,后续如果主从同步中断,恢复起来会更简单。
第二步:修改从库配置文件(my.cnf或my.ini),配置并行复制参数。
找到从库的配置文件,不同操作系统的配置文件路径不一样,Linux系统一般在/etc/my.cnf,Windows系统一般在MySQL的安装目录下,找到[mysqld]节点,在下面添加以下参数:
# 开启并行复制,设置worker线程数量(根据CPU核心数调整)
slave_parallel_workers = 8
# 并行复制类型:基于逻辑时钟
slave_parallel_type = LOGICAL_CLOCK
# binlog格式设置为ROW,确保同步准确
binlog_format = ROW
# 开启GTID模式,确保数据一致性
gtid_mode = ON
enforce_gtid_consistency = ON
# 中继日志信息存储在表中,提升稳定性
relay_log_info_repository = TABLE
# 复制信息存储在表中,避免文件损坏导致同步失败
master_info_repository = TABLE
# 增大复制缓冲区,提升binlog传输速度
slave_pending_jobs_size_max = 1G
# 确保从库提交顺序与主库一致(可选,根据业务需求调整)
slave_preserve_commit_order = ON
这里有几个注意点,大家一定要记住,避免配置出错:
(1)slave_parallel_workers的取值:不要设置为0(关闭并行复制),也不要设置为1(徒增调度开销,和单线程没区别),建议根据CPU核心数调整,比如4核CPU设置为4,8核CPU设置为8或16,16核CPU设置为16或32,按需调整。
(2)slave_preserve_commit_order参数:MySQL 5.7默认是OFF,MySQL 8.0默认是ON,这个参数的作用是保证从库的事务提交顺序和主库一致,对于需要强一致性的业务(比如电商下单、支付),建议开启;如果业务对一致性要求不高(比如日志同步),可以关闭,能提升一点执行速度。
(3)修改配置文件后,需要重启从库,参数才能生效,建议在业务低峰期操作,比如凌晨,避免影响业务正常运行。
第三步:重启从库,启动并行复制。
重启从库的命令,根据操作系统不同,命令略有差异,给大家整理好了,直接复制使用即可:
Linux系统:systemctl restart mysqld
Windows系统:在服务中找到MySQL,右键重启服务即可。
第四步:验证并行复制是否生效。
重启后,登录从库,执行以下SQL命令,验证并行复制是否正常生效:
-- 查看从库同步状态,重点看Seconds_Behind_Master(延迟时间)
show slave status\G;
-- 查看worker线程是否在运行
show processlist;
-- 查看并行复制的详细状态
SELECT * FROM performance_schema.replication_applier_status_by_worker;
如果看到以下情况,说明并行复制已经生效,配置成功:
(1)show processlist命令中,出现多个“Slave_worker”线程(数量和slave_parallel_workers设置的一致),说明worker线程已经正常启动。
(2)replication_applier_status_by_worker表中,多个worker线程的LAST_SEEN_TRANSACTION字段在不断更新,说明这些线程在正常执行事务。
(3)Seconds_Behind_Master字段的值明显下降,从之前的几十秒、几分钟,降到几秒甚至1秒以内,说明同步速度提升了。
如果没有生效,大家可以检查一下配置参数是否修改正确,或者重启从库后再重新验证。
2. MySQL 8.0版本:并行复制优化,默认开启,更稳定高效
MySQL 8.0版本对并行复制进行了大幅优化,默认就开启了并行复制,而且采用了更先进的“WRITESET”并行复制技术,比5.7版本的逻辑时钟并行复制更高效、更稳定,配置也更简单,不用做太多调整。
WRITESET并行复制的核心逻辑:它会给每个事务生成一个“写集合”(WRITESET),也就是这个事务修改的数据行的唯一标识(比如主键),如果两个事务的写集合没有交集,说明它们没有依赖关系,可以并行执行;如果有交集,说明有依赖关系,只能顺序执行。这种方式比逻辑时钟更精细,能实现更高的并行度,尤其是在高并发场景下,效果更明显。
MySQL 8.0版本的并行复制配置,比5.7更简单,因为很多参数默认就已经配置好了,咱们只需要微调即可,具体步骤如下:
第一步:查看当前从库的并行复制配置。
登录从库,执行以下SQL命令,查看当前的并行复制相关参数:
show variables like '%slave_parallel%';
show variables like 'binlog_format';
show variables like 'gtid_mode';
show variables like 'binlog_transaction_dependency_tracking';
MySQL 8.0默认的关键参数,大家可以参考一下,心里有个数:
(1)slave_parallel_workers:默认是4,已经开启并行复制,我们可以根据CPU核心数调整,比如8核CPU设置为8或16,16核CPU设置为16或32,按需调整。
(2)slave_parallel_type:默认是“LOGICAL_CLOCK”,但实际上,MySQL 8.0默认启用的是WRITESET并行复制,因为binlog_transaction_dependency_tracking参数默认是“WRITESET”。
(3)binlog_format:默认是“ROW”格式,符合并行复制的要求,不用修改。
(4)gtid_mode:默认是“ON”,开启GTID模式,确保数据一致性,不用修改。
(5)binlog_transaction_dependency_tracking:默认是“WRITESET”,开启WRITESET并行复制,不用修改。
第二步:微调配置文件,优化并行复制性能。
虽然默认配置已经能用,但我们可以根据业务情况,微调以下参数,进一步提升并行复制的性能,让同步速度更快、更稳定:
# 调整worker线程数量(根据CPU核心数调整)
slave_parallel_workers = 8
# 开启WRITESET并行复制(默认已开启)
binlog_transaction_dependency_tracking = WRITESET
# 允许worker线程并行提交(提升执行速度)
slave_parallel_commits = 10
# 增大复制缓冲区,提升binlog传输速度
slave_pending_jobs_size_max = 2G
# 关闭从库的写操作(避免手动写入导致数据不一致)
read_only = ON
super_read_only = ON
这里重点说一下slave_parallel_commits参数:这个参数表示worker线程可以并行提交的事务数量,默认是1,设置为10(或更高),可以让多个worker线程同时提交事务,进一步提升执行速度,尤其是在高并发场景下,效果很明显,大家可以根据自己的业务情况调整。
另外,开启read_only和super_read_only参数,是为了禁止手动往从库写入数据,避免从库数据和主库不一致,导致并行复制出错,这个参数一定要开启。
第三步:重启从库,验证并行复制效果。
重启从库后,执行以下SQL命令,验证并行复制是否正常:
show slave status\G;
show processlist;
SELECT * FROM performance_schema.replication_applier_status_by_worker;
-- 查看WRITESET并行复制的状态
SELECT * FROM performance_schema.replication_asynchronous_connection_failover;
如果看到多个Slave_worker线程在运行,Seconds_Behind_Master的值稳定在1秒以内,说明并行复制已经正常生效,而且性能比5.7版本更优,同步速度更快、更稳定。
3. 并行复制避坑要点,避免数据不一致和性能下降
很多人配置完并行复制后,不仅没有提升速度,反而出现了数据不一致、同步中断的问题,这都是因为踩了一些坑,咱们总结几个常见的坑,大家一定要避开,少走弯路:
坑1:binlog格式不是ROW格式,导致并行复制数据不一致。
解决方案:无论哪个版本,并行复制都必须把binlog_format设置为ROW格式,这是前提,否则会出现同步不一致的问题,这个前面已经反复强调过,大家一定要记住。
坑2:slave_parallel_workers设置太大,导致线程上下文切换频繁。
解决方案:不要盲目增加worker线程数量,建议根据CPU核心数调整,一般是CPU逻辑核数的1-2倍,比如4核CPU设置为4,8核CPU设置为8或16,设置太大反而会降低效率,得不偿失。
坑3:事务之间有依赖关系,导致并行复制自动降级为单线程。
比如,两个事务都操作了同一张表的同一行数据,这两个事务就有依赖关系,从库的worker线程无法并行执行,只能顺序执行,这时候并行复制就会自动降级为单线程,延迟不会下降,相当于白配置了。
解决方案:尽量避免多个事务操作同一行数据,比如在高并发场景下,用分库分表的方式,分散数据,减少事务之间的依赖;另外,避免使用“INSERT ... SELECT”“CREATE TABLE ... AS SELECT”这类语句,这类语句会被标记为不可并行,导致并行复制降级。
坑4:从库开启了写操作,导致数据不一致。
如果从库允许手动写入数据,或者有业务逻辑往从库写入数据,就会导致从库数据和主库不一致,并行复制会出现错误,同步中断,延迟瞬间暴涨。
解决方案:开启从库的read_only和super_read_only参数,禁止手动写入和超级用户写入,确保从库的数据只能通过主从同步获得,避免数据不一致。
坑5:主从版本不一致,导致并行复制无法正常工作。
解决方案:主从库版本尽量保持一致,至少主库版本不高于从库版本,避免出现兼容性问题,影响并行复制,前面的案例已经给大家提醒过,这里就不再重复了。
坑6:没有开启GTID模式,导致同步故障难以恢复。
解决方案:建议开启GTID模式,GTID能确保主从数据的一致性,简化故障恢复,尤其是在并行复制场景下,开启GTID模式能提升同步的稳定性,避免出现同步中断后难以恢复的情况。
以上就是并行复制的详细配置和避坑要点,只要按照这个方法配置,从库的执行速度会翻倍,延迟会大幅下降。但只靠并行复制还不够,如果有大事务存在,即使开启了并行复制,大事务还是会占用worker线程很久,导致其他事务排队,延迟依然会存在。所以,我们还需要第二个核心方案:事务拆分,把大事务拆成小事务,从根源上解决大事务的拖累。
(二)事务拆分:把大事务“拆成小块”,避免同步卡顿
事务拆分的核心逻辑:把一个耗时久、影响数据量大的大事务,拆分成多个耗时短、影响数据量小的小事务,每个小事务独立提交,这样主库写binlog的时候,会分批次写入,从库下载和执行的时候,也能分批次处理,不会被一个大事务卡住,同步速度会大幅提升,而且能避免数据不一致的问题。
比如,之前提到的“批量更新50万条用户积分”的大事务,执行时间需要15分钟,拆分成50个小事务,每个事务更新1万条数据,每个小事务的执行时间只有18秒,这样主库会分50次写入binlog,每次binlog的体积很小,从库的worker线程可以并行执行这些小事务,不会被一个大事务卡住,延迟自然就降下来了。
事务拆分不是“随便拆分”,需要遵循两个核心原则,大家一定要记住,否则会出现数据不一致的问题:一是“无依赖”,拆分后的小事务之间,不能有依赖关系(比如不能前一个小事务的结果,影响后一个小事务的执行);二是“原子性”,每个小事务都要保证原子性,要么全部执行成功,要么全部失败,避免出现数据不一致。
接下来,咱们讲具体的拆分方法,结合实际案例,分场景来讲,大家可以直接套用,不管是批量更新、批量插入,还是复杂业务事务,都能用到。
1. 批量更新/删除:按主键分段拆分,分批次执行
这是最常见的场景,比如批量更新用户积分、批量删除历史订单、批量修改商户状态、批量更新商品价格等,这类操作的特点是:操作的是同一张表,数据量大,没有依赖关系,适合按主键分段拆分,简单又高效。
案例:批量更新用户表(user)中,所有用户的积分(points)增加100,用户表有50万条数据,主键是user_id(自增),如果不拆分,执行时间会很长,拖慢主从同步。
不拆分的大事务(错误示例):
-- 大事务:一次性更新50万条数据,执行时间长,拖慢同步
BEGIN;
UPDATE user SET points = points + 100;
COMMIT;
这种写法虽然简单,但会生成一个巨大的事务,主库执行需要十几分钟,从库执行也需要十几分钟,同步延迟会瞬间暴涨,非常不可取,大家一定要避免。
拆分后的小事务(正确示例):
按user_id分段,每批更新1万条数据,分50批执行,每个批次一个独立的事务,具体可以用存储过程实现,方便批量执行,不用手动一批一批操作,节省时间和精力。
-- 存储过程:批量更新用户积分,分批次执行
DELIMITER//
-- 批量更新用户积分:P_ADD为新增积分,P_BATCH为每批处理条数(默认10000)
CREATE PROCEDURE BATCH_UPDATE_USER_POINTS(IN P_ADD INT, IN P_BATCH INT DEFAULT 10000)
BEGIN
DECLARE V_START_ID INT DEFAULT 1; -- 起始ID
DECLARE V_MAX_ID INT; -- 最大用户ID(确定循环边界)
-- 获取用户表最大ID,确定循环的终止条件
SELECT MAX(USER_ID) INTO V_MAX_ID FROM USER;
-- 循环分批次更新
WHILE V_START_ID <= V_MAX_ID DO
-- 更新当前批次数据,按主键分段,避免重复和遗漏
UPDATE USER
SET POINTS = POINTS + P_ADD
WHERE USER_ID BETWEEN V_START_ID AND V_START_ID + P_BATCH - 1;
COMMIT; -- 每批更新后立即提交,缩短事务时长,减少锁占用
SET V_START_ID = V_START_ID + P_BATCH; -- 更新下一批起始ID
SELECT SLEEP(0.1); -- 暂停100ms,避免高频操作压垮数据库IO和CPU
END WHILE;
END//
DELIMITER ;
-- 调用示例:给所有用户加100积分,每批处理10000条
CALL BATCH_UPDATE_USER_POINTS(100, 10000);
这样拆分后,每个小事务只更新1万条数据,执行时间只有十几秒,主库会分50次写入binlog,每次binlog的体积很小,从库的worker线程可以并行执行这些小事务,不会被卡住,同步速度会大幅提升,而且能避免大事务导致的锁冲突。
这里有几个注意点,大家一定要记住,避免拆分出错:
(1)每批处理的条数,根据业务情况调整,一般建议每批1000-10000条,太多会导致小事务变成“中等事务”,还是会拖慢同步;太少会增加事务提交的次数,增加主库的压力,按需调整即可。
(2)添加SLEEP(0.1),暂停100毫秒,避免高频提交事务,压垮主库的IO和CPU资源,尤其是数据量很大的时候,这个步骤很重要。
(3)拆分的时候,一定要按主键分段,因为主键是唯一的,不会出现重复更新或遗漏更新的情况,而且主键索引查询速度快,能提升更新效率,避免全表扫描。
(4)如果是批量删除,方法和批量更新一样,也是按主键分段,分批次执行,避免一次性删除大量数据,生成大事务。
2. 批量插入:分批次插入,避免一次性插入大量数据
批量插入场景也很常见,比如导入历史数据、批量导入用户信息、批量插入订单数据、批量导入商品信息等,一次性插入几万、几十万条数据,会生成一个大事务,拖慢主从同步,还会占用大量的磁盘和内存资源。
案例:批量插入10万条订单数据(order表),订单表的主键是order_id(自增),如果不拆分,一次性插入,执行时间会很长,binlog体积也很大,拖慢同步。
不拆分的大事务(错误示例):
-- 大事务:一次性插入10万条数据,执行时间长,binlog体积大
BEGIN;
INSERT INTO order (order_id, user_id, order_time, amount)
VALUES (1, 1001, '2026-04-09 10:00:00', 199),
(2, 1002, '2026-04-09 10:01:00', 299),
... -- 省略99998条数据
(100000, 100000, '2026-04-09 12:00:00', 399);
COMMIT;
这种写法,一次性插入10万条数据,主库执行需要几分钟,binlog体积会达到几十MB,从库下载和执行都需要很长时间,同步延迟会瞬间暴涨,大家一定要避免。
拆分后的小事务(正确示例):
分10批插入,每批插入1万条数据,每个批次一个独立的事务,具体可以用程序代码(比如Java、Python)实现,也可以用存储过程实现,这里给大家提供存储过程的示例,方便大家直接使用。
-- 存储过程:批量插入订单数据,分批次执行
DELIMITER//
CREATE PROCEDURE BATCH_INSERT_ORDER(IN P_BATCH_SIZE INT DEFAULT 10000)
BEGIN
DECLARE V_BATCH_COUNT INT DEFAULT 0; -- 批次计数器
DECLARE V_MAX_NUM INT DEFAULT 100000; -- 总插入条数
DECLARE V_START INT DEFAULT 1; -- 起始订单ID
-- 初始化行号变量,用于生成连续的订单ID和用户ID
SET @rownum = 0;
WHILE V_START <= V_MAX_NUM DO
-- 插入当前批次数据,每批插入P_BATCH_SIZE条
INSERT INTO `order` (order_id, user_id, order_time, amount)
SELECT
V_START + t.rn - 1, -- 订单ID,连续递增
10000 + V_START + t.rn - 1, -- 用户ID,避免和现有用户ID冲突
DATE_ADD(NOW(), INTERVAL (t.rn - 1) MINUTE), -- 订单时间,逐分钟递增
FLOOR(100 + RAND() * 500) -- 订单金额,随机生成100-600之间的整数
FROM (
-- 生成连续的行号,用于批量生成数据
SELECT @rownum := @rownum + 1 AS rn
FROM information_schema.tables t1, information_schema.tables t2
LIMIT P_BATCH_SIZE
) t;
COMMIT; -- 每批插入后立即提交,缩短事务时长
SET V_START = V_START + P_BATCH_SIZE; -- 更新下一批起始ID
SET V_BATCH_COUNT = V_BATCH_COUNT + 1; -- 批次计数器加1
SELECT SLEEP(0.2); -- 暂停200ms,缓解主库IO压力
END WHILE;
-- 输出插入结果,方便查看执行情况
SELECT CONCAT('插入完成,共插入', V_MAX_NUM, '条数据,分', V_BATCH_COUNT, '批执行') AS result;
END//
DELIMITER ;
-- 调用示例:每批插入10000条,共插入100000条数据
CALL BATCH_INSERT_ORDER(10000);
这样拆分后,每个小事务只插入1万条数据,执行时间短,binlog体积小,从库可以并行执行这些小事务,同步速度会大幅提升,而且能避免因为一次性插入大量数据导致的主库IO压力过大,防止主库卡顿。
这里有几个注意点:
(1)批量插入的时候,尽量使用“INSERT INTO ... SELECT”的方式,比“INSERT INTO ... VALUES”的方式效率更高,尤其是数据量较大的时候。
(2)每批插入的条数,根据主库的IO能力调整,一般建议每批1000-10000条,避免一次性插入太多,导致主库IO瓶颈。
(3)添加SLEEP(0.2),暂停200毫秒,缓解主库的IO压力,避免主库因为高频插入而卡顿。
3. 复杂业务事务:按业务逻辑拆分,拆分无依赖的步骤
有些大事务,不是单纯的批量操作,而是包含多个业务步骤,比如用户下单流程:创建订单、扣减库存、扣减余额、添加积分、发送消息,这些步骤如果放在一个事务里,就会变成一个大事务,执行时间长,而且一旦某个步骤出错,整个事务都会回滚,影响效率,还会拖慢主从同步。
案例:用户下单流程,包含5个步骤,原本放在一个大事务里,执行时间需要5秒,拆分后,每个步骤一个独立的小事务,执行时间总共不超过1秒,同步速度大幅提升,而且能避免一个步骤出错导致整个事务回滚。
不拆分的大事务(错误示例):
-- 大事务:包含多个业务步骤,执行时间长,拖慢同步
BEGIN;
-- 1. 创建订单
INSERT INTO order (order_id, user_id, amount, status) VALUES (10001, 1001, 199, 'pending');
-- 2. 扣减库存
UPDATE product SET stock = stock - 1 WHERE product_id = 101;
-- 3. 扣减用户余额
UPDATE user SET balance = balance - 199 WHERE user_id = 1001;
-- 4. 添加用户积分
UPDATE user SET points = points + 20 WHERE user_id = 1