从主库提交到从库可见,中间那些看不见的环节,往往藏着数据一致性的大问题。我之前在项目里经历过一次主从切换后数据对不上的事故,排查到最后才发现问题出在binlog格式和并行复制参数上,而不是网络或硬件。今天把这块彻底讲清楚。
1. 主从同步的骨架:一条日志在三个节点间的流转
1.1 binlog:实时性和有序性都从这里开始
MySQL的主从同步,本质是主库把binlog日志传给从库,从库把binlog里的变更重新执行一遍。这个机制听起来简单,但几乎所有关于“实时性”和“有序性”的问题,根源都在binlog身上。
binlog是MySQL的二进制日志,记录的是数据库中实际发生的变更操作。它有两种核心格式:STATEMENT格式记录的是SQL语句本身,也就是“我执行了一条UPDATE”;ROW格式记录的是每一行数据变更前后的完整镜像,也就是“哪一行从什么值变成了什么值”。MySQL 8.0默认使用ROW格式,这也是生产环境最推荐的格式。
为什么说binlog是所有实时性和有序性的源头?因为从库看到的数据变化顺序,完全取决于binlog里事件的排列顺序。主库上事务提交的先后顺序,决定了binlog里事件的先后顺序;而从库只需要严格按照binlog的顺序去重放,就能在最终状态上跟主库保持一致。所以,主从同步的有序性,本质上是一个“日志顺序”的问题。
你可以把binlog想象成快递流水线上的包裹队列,每个包裹都有一个编号。主库是发货仓,从库是收货仓。只要包裹按编号依次发出、依次到达,收货仓理货时也按编号依次处理,那两边看到的货物状态就永远一致。
1.2 从库的两个线程:IO线程负责搬家,SQL线程负责装修
从库一侧有两件独立的事情要做:一是从主库把binlog搬过来,二是把搬过来的binlog执行掉。MySQL用两个线程把这两件事分开,分别是IO线程和SQL线程。
IO线程负责连接到主库,发送请求,主库端会为这个连接启动一个专门的dump线程,把binlog按顺序一条条推送给从库。IO线程收到这些日志后,把它们原样写入从本地的relay log(中继日志),然后IO线程的工作就结束了。
SQL线程则负责读取relay log,把里面的binlog事件解析成真实的写操作,在从库上执行。这个“执行”不是直接跑一遍当时的SQL,而是根据binlog里的记录,把对应行的数据改成对应值。如果是ROW格式,就是找到那一行,把旧值改为新值。
这两个线程的设计非常巧妙。想象一个场景:从库的磁盘正在做大量写入,SQL线程回放速度变慢,这时候如果IO线程也被卡住,主库的binlog就送不过来,主从延迟会进一步扩大。但因为有了relay log这一层缓冲,IO线程可以先把日志接收下来存放在本地,SQL线程慢点没关系,主库那边不用一直等。这就是“搬家”和“装修”分离的意义。
1.3 relay log:把网络接收和数据回放彻底解耦
很多人会忽略relay log的存在,但它其实是整个主从同步架构里保证实时性的关键设计。它让“从主库拉取日志”和“在从库执行日志”这两个操作互不阻塞。
我从实际维护的角度说几个relay log相关的坑。第一,relay log默认最大能到1GB,如果从库长时间断连后重新连上,主库会把断连期间的binlog一次性推送过来,relay log会迅速膨胀,这时候要注意磁盘空间。第二,relay log是会被SQL线程消费后自动清理的,但如果有异常导致SQL线程卡住,relay log会持续增长,磁盘满了之后IO线程也跟着罢工,整个复制链路就断了。
还有一个常见问题:从库重启或者崩溃后,relay log可能处于不一致的状态。MySQL提供了relay_log_recovery参数,默认开启。开启后,从库会自动丢弃损坏或者未完成的relay log,重新从主库拉取,确保复制能干净地继续。这个参数建议保持开启,我见过一些环境因为手动关掉它,重启后出现relay log和binlog位点对不上的诡异故障。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实时性的三道闸门:从主库提交到从库可见发生了什么
2.1 异步、半同步、同步:一条数据要等多久才返回
主从同步的实时性,首先取决于一个核心语义:主库提交事务时,要不要等从库回复。这决定了你写入主库的数据,最快多久能被从库看到。
默认的异步复制模式下,主库执行完事务并提交成功后,就直接返回客户端“成功了”。binlog虽然会发送给从库,但主库完全不关心从库收到没有。这种模式的优点是主库性能几乎不受影响,缺点是如果主库在binlog发送前宕机,从库可能永远收不到这个事务,主从数据就不一致了。
半同步复制在此基础上加了一道保障:主库提交事务时,会等待至少一个从库确认“我已经收到了binlog”,然后才给客户端返回成功。这里要注意,从库确认的是“收到并写入relay log”,而不是“已经执行完”。所以在半同步复制下,数据从主库到从库的“可见时间”仍然有延迟,但至少保证了在无故障情况下,binlog不会丢。
全同步复制是所有从库都执行完才返回,一致性最强,但性能代价巨大,MySQL官方没有内置,需要借助中间件或者分布式事务方案来实现,一般业务根本用不到,所以大部分生产环境在半同步和异步之间做选择。
我之前推荐的做法是:业务如果对数据一致性要求高,比如订单、支付相关,主库至少开启半同步复制;如果只是缓存、报表、日志类场景,异步复制就够了。
2.2 延迟来源盘点:不是只有网络
主从延迟是所有DBA都会遇到的问题,但它的来源远不止“网络慢”这么简单。我梳理一下实际工作中最常见的几个延迟来源:
大事务是最容易踩的坑。一个UPDATE语句如果扫描了上百万行,它生成的binlog可能就有几十上百MB。从库SQL线程必须把这个大事务在relay log里完整读取出来,再逐行执行,期间它无法处理后续的事务。所以在主库上执行大事务时要格外小心,尽量拆批,不然主从延迟分分钟拉满。
从库磁盘性能差。relay log的写入和读取、SQL线程回放时对数据页的修改,都需要从库的磁盘做IO。如果从库用的是普通ECS云盘,而主库用的是高性能SSD,且从库还承担了大量查询流量,SQL线程和用户查询之间会出现IO争抢,延迟就会被放大。这也是为什么我总是建议从库硬件配置不要低于主库。
没有主键的表是回放杀手。ROW格式下,从库执行变更时需要找到目标行。如果表没有主键,可能会走到全表扫描来定位每一行,这个开销可能是主库执行时的几十倍。这个问题的解决方式很简单:建表时强制要求主键。
主库的dump线程和从库IO线程处理不过来。一台主库挂了几十个从库,每个从库都占用一个dump线程,如果主库的binlog写入量很大,dump线程也会成为瓶颈。这个时候可能需要考虑引入级联复制,也就是让部分从库从另一个从库同步,而不是全部直连主库。
2.3 并行复制:从单线程回放到组内并行
在MySQL 5.6之前的复制架构里,从库只有一个SQL线程,它必须一条条按顺序执行relay log里的事件。这意味着,从库的回放速度最多等于单个线程的执行速度,即使CPU有几十个核也帮不上忙。这在写入量大的场景下是致命的,主从延迟几乎无法避免。
MySQL 5.6引入了并行复制,但当时的并行粒度是库级别:不同schema下的表可以由不同SQL线程执行。如果业务只有一个库,或者绝大多数写入都集中在同一个库,并行复制就形同虚设。
MySQL 5.7把并行粒度提升到了事务级别,这背后依赖的是主库的组提交机制。简单说,主库在同一批次组提交的事务是互不冲突的,既然它们能同时进入组提交,说明在执行阶段没有相互争抢资源,那从库回放时也可以并行执行这些事务。从库SQL线程会识别binlog中标记的commit order信息,把同组事务分发给多个worker并行应用。
到了MySQL 8.0,并行复制的调度算法更进一步,引入了Writeset机制,通过在binlog中记录事务修改过的行信息(写集合),可以在更宽松的条件下判断两个事务之间是否有冲突,从而让更多事务获得并行执行的机会。
但并行复制不是免费的,它和“有序性”之间存在天然张力,我后面专门用一节讲这个平衡。
3. 有序性不是玄学:binlog顺序为何等于提交顺序
3.1 两阶段提交:binlog和redo log到底在协调什么
要理解MySQL主从同步的有序性,必须先理解主库内部在提交事务时做的一个关键操作:两阶段提交。
InnoDB有自己的重做日志redo log,MySQL服务器层面又有binlog。redo log用于崩溃恢复时把数据页恢复到一致状态,binlog用于主从复制和基于时间点的恢复。问题来了:这两份日志如果顺序不一致,崩溃时可能出现“redo log里事务已经提交,但binlog里没有这个事务”的情况,主从不一致就随之而来。
MySQL的解决方案是:在事务提交过程中,先把事务的变更写入redo log并标记为prepare状态;然后写binlog;最后再把redo log标记为commit状态。这个流程叫两阶段提交。
它的巧妙之处在于:崩溃恢复时,MySQL会检查binlog中是否存在这个事务。如果binlog里已经写入了,说明事务应该被提交;如果binlog里没有,说明事务还没真正提交成功,丢弃即可。这样,binlog和InnoDB数据就始终能在同一条时间线上对齐。
所以,真正的有序性是靠“两阶段提交”这个机制锁死的:binlog里有没有这个事务记录,决定了这个事务最终是否存在。
3.2 组提交:批量合并写入但不打乱顺序
较早版本的MySQL每次提交事务都要刷一次binlog到磁盘,这个fsync操作代价很高。在高并发场景下,性能压力很大。MySQL 5.6引入了组提交:多个事务在提交阶段合并成一组,只做一次fsync,显著提升吞吐量。
组提交会带来一个有趣的问题:如果多个事务同时提交,顺序怎么定?实际上,组提交并不是所有事务同时到达,而是在提交的临界段有一个队列。事务按进入队列的先后顺序排队,第一个触发刷盘的事务会带着同一时间窗口内后来的其他事务一起提交。写binlog时,这组事务是严格按照队列顺序写入的。
换句话说,组提交并不改变事务在binlog中的顺序,它只是把多个事务的磁盘写入动作合并成了批量操作。所以即使开了组提交优化,主库上事务的提交顺序,依然严格等于binlog里事件的排列顺序。
这里有个容易误会的点:所谓“提交顺序”,并不是客户端调用“提交”语句的代码执行顺序,也不是客户端是否先发起请求的顺序,而是事务真正进入binlog写入临界区的顺序。binlog_group_commit_sync_delay参数如果设置得较大,主库会故意延迟一点点时间再刷盘,让更多事务凑进同一组,提高吞吐量,但代价是增加了单次提交的延迟。这个参数在追求极致性能时可以微调,我一般建议设置在几十到几百微秒的量级,不要超过1毫秒,否则业务侧写入延迟会明显上升。
3.3 并行复制如何不破坏“组间有序”
5.7之后的并行复制,让从库回放不再是一条道走到黑。但并行意味着多个事务同时执行,这是否会破坏有序性?
答案是不会,因为并行是有条件、有边界的。MySQL的并行复制策略是:主库binlog中记录了每个事务的last_committed和sequence_number这两个序号。简单理解,同一批次组提交的事务拥有相同的last_committed,它们是互不依赖的,所以从库可以把它们交给多个worker并行执行。
但是,另一批事务必须等同一批内所有worker都执行完成后,才能开始执行下一批。也就是说,并行只是在同组事务之间发生,而组和组之间依然保持严格的前后顺序。这样既获得了并行度,又保证了最终的一致性和有序性。
还有一个参数需要注意:slave_preserve_commit_order。它控制从库在并行执行事务时,是否严格按照relay log中的顺序提交事务。开启这个参数后,虽然事务在worker线程里是并行执行的,但提交时会按顺序排队,保证从库上最终能观察到的“提交顺序”和relay log完全一致。如果你对一致性要求比较高,建议开启。
这带来一个“短事务被长事务阻塞”的问题:如果一组事务里混入了一个超大事务,其他短事务即使执行完了,也要等大事务执行完才能一起进入下一批。所以,大事务不仅在主库上危险,在从库并行复制中同样是秩序的破坏者。
4. 实战排查:延迟上去的时候我怎么定位
4.1 几个真实的大延迟场景
第一个场景:一次无意的全表更新。项目里有同事在测试环境直接执行了一条不带WHERE条件的UPDATE,整张表300万行数据被更新成同一个值。这条SQL在主库执行了大概5秒,但生成的binlog有接近1GB。从库SQL线程回放这1GB的binlog花了10分钟,期间从库上所有读请求全部面临数据滞后。解决方案只能等它回放完,之后我们在生产环境加了SQL审计策略,禁止不带条件的UPDATE和DELETE。
第二个场景:从库磁盘满了。relay log因为长期没清理,加上主库当天有大事务写入,relay log把从库数据盘打满了。磁盘满之后SQL线程写不了relay log,复制直接中断。排查时看SHOW SLAVE STATUS,发现Slave_IO_Running: Connecting,从库一直处于重连状态。当时没有自动告警,等用户反馈查询数据不对才知道。
第三个场景:主从机器CPU型号不同。从库是老的物理机,主库是最新云主机,主库写入QPS 3000左右,从库SQL线程单核使用率已经100%。打开并行复制之后,从库回放速度才跟上来。
4.2 Seconds_Behind_Master为什么不能全信
很多人的第一反应是看SHOW SLAVE STATUS里的Seconds_Behind_Master字段,用它来判断主从延迟有多大。但用过一段时间后你会发现,这个字段有时候会骗人。
它的计算逻辑大致是:从库当前时间减去SQL线程正在执行的事务在binlog里的时间戳。注意,binlog事件里的时间戳是主库执行该事务的时间。如果从库和主库的系统时间不一致,这个值本身就失真。更麻烦的是,如果relay log已经全部消费完了,SQL线程空闲,Seconds_Behind_Master会显示为0,给人一种“数据已经追平”的假象,实际上只要下一批binlog还没到达,后面依然有延迟的空间。
更可靠的做法是用pt-heartbeat,它会在主库上周期性地更新一张心跳表,从库通过读取心跳表的最新时间戳,和从库本地时间比对。这个方法不受binlog事件时间戳和relay log消费进度的干扰,能比较真实地反映主从之间数据的时间差。实际运维中,我建议把pt-heartbeat和Seconds_Behind_Master一起看,两个指标互相印证。
4.3 从库数据的一致性核对与修复
延迟不是最可怕的,最可怕的是延迟之外还出现了数据不一致。造成不一致的原因有很多:binlog格式用了STATEMENT且函数是非确定性的、从库上有人直接改过数据、从库回放过程中被kill等。
常用工具是Percona Toolkit里的pt-table-checksum。它会按主键分块,在主库上计算每一块数据的校验和,然后在从库上计算同样查询的校验和,把不一致的块报告出来。整个过程对在线业务的影响很小,可以设置在业务低峰期执行。
找到不一致数据后,用pt-table-sync修复。它会把主库的数据变更SQL重新生成一遍,应用到从库上。
但要注意一个稳妥性的问题:修复前最好先备份从库数据,而且修复过程要选择业务低峰期执行。如果从库一边在回放binlog,一边在被pt-table-sync修改数据,两个写入流可能互相打架,产生新的不一致。
5. 把实时性和有序性调好的关键参数
5.1 主库侧的取舍
先看主库。主库的核心责任是产生完整、有序的binlog,同时尽量不影响业务写入性能。
sync_binlog=1是必须的。这个参数控制每次提交事务后,是否立即把binlog刷到磁盘。为0时OS可能推迟落盘,如果主库宕机,至少有最近的binlog丢失,从库就会缺数据。为1时每次提交都落盘,配合innodb_flush_log_at_trx_commit=1,才能保证事务在主库和从库的一致性。性能代价肯定有,但数据安全比性能重要。
binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count是组提交的微调参数。前者表示延迟多少微秒再刷盘,后者表示凑够多少个事务就提前刷盘。典型配置是binlog_group_commit_sync_delay=1000左右,再设置binlog_group_commit_sync_no_delay_count=10。它们会稍微增加单事务的返回时间,但能显著减少磁盘fsync的频率。
binlog_order_commits默认开启,它强制事务提交顺序和binlog写入顺序一致。除非你对性能有极其苛刻的要求,否则不要关闭它。
5.2 从库侧的设计参数
从库的核心问题是“如何更快、更安全地消费relay log”。
relay_log_recovery=ON要开启,这个前面已经提到过。
并行复制参数方面,MySQL 8.0里默认的replica_parallel_workers(即之前的slave_parallel_workers)为0,也就是串行。建议设置为CPU核心数的一半或更多,常见配置是4或8。slave_parallel_type在8.0里默认就是LOGICAL_CLOCK(逻辑时钟),不要改回DATABASE,否则并行粒度会退化。
开启并行复制后,务必设置slave_preserve_commit_order=ON,保证回放事务的提交顺序。同时要注意,如果从库上并行复制已经开启,而主库开启了binlog_transaction_dependency_tracking=WRITESET,从库能识别到更多无依赖事务,并行度会更高。这个参数在8.0中默认是COMMIT_ORDER,可以视情况调整为WRITESET或WRITESET_SESSION。
从库如果承担读流量,还要关注innodb_buffer_pool_size的设置,尽量让热数据都在内存里,减少SQL线程回放时的磁盘读取。
5.3 常见面试追问与我的回答思路
关于主从同步的实时性和有序性,面试官有不少喜欢追问的细节,我整理一下典型的几个。
“主库上事务T1先开始执行,T2后开始执行,但T2先提交,binlog里顺序是什么?”答案是后提交的T2在binlog里排在前面。binlog顺序只看提交顺序,不看事务开始执行顺序。因为事务执行期间不写binlog,只有在提交阶段排队时才会写入。
“为什么半同步复制下,从库可能还没执行完,主库就返回成功了?”因为半同步等待的是从库IO线程“写入relay log”的确认,而不是SQL线程“完成回放”。从库收到日志和真正执行之间,还有一段距离。
“如果从库的SQL线程在执行一个大事务,其他事务会不会被阻塞?”会的。大事务执行期间,后续事务即使没有冲突,也要等它完成。并行复制也不会打破这个限制,因为大事务所在的那一批事务之后的所有批次都必须等待。
“主从切换时怎么确保不丢数据和重复数据?”如果想不丢,用半同步或者全同步,同时切换前确认从库已接收主库最后的binlog位点。如果想不重复,业务层需要做幂等,或者记录好切换时的GTID位置,接入新主库时从对应位置继续订阅。
“GTID对有序性有帮助吗?”GTID本身不改变执行顺序,但能精确标识每个事务,简化主从切换时的位点定位。如果没有GTID,切换时靠文件名和偏移量,比较容易搞错位置,用了GTID之后,从库能自动跳过已经执行过的事务,减少重复执行的风险。
我在实际项目中,通常会把GTID模式开起来,同时结合半同步复制和并行复制来搭建主从环境。一套合理的基线配置大概是:主库sync_binlog=1、innodb_flush_log_at_trx_commit=1、开启半同步;从库relay_log_recovery=ON、replica_parallel_workers=8、replica_parallel_type=LOGICAL_CLOCK、replica_preserve_commit_order=ON、开启GTID。这套组合在我维护的几个线上项目里跑了两三年,稳定性很好。
最后分享一个小技巧:当从库出现延迟时,别急着改参数,先用SHOW ENGINE INNODB STATUS看看当前SQL线程到底卡在什么语句上,再结合binlog里的事务大小来判断是资源问题还是大事务问题。我见过不少团队把replica_parallel_workers调大好几倍,结果延迟没降,反而因为worker线程之间调度开销变大,回放速度更慢了。优化之前先定位,永远是排障的第一原则。
