在MySQL GTID复制架构里,最让人半夜惊醒的,就是从库的SQL线程突然停了,报错信息里堆着一串看不懂的事务号。尤其是你刚把复制架构切到GTID模式,正觉得一身轻,结果一条Duplicate entry或者Can't find record就把整条复制链路卡死。这时候你大概率会想:能不能直接告诉从库“跳过这个报错的事务”?当然可以,但跳过之前要是没想清楚,后面主从数据不一致的锅还得你来背。
这篇文章把我实际处理这类问题的完整流程写出来:从确认该不该跳、找到报错GTID,到用空事务注入法安全跳过,再到跳完之后的校验和修复,最后附上常见的坑和批量处理的脚本。适合正在维护MySQL集群的DBA、运维同学,以及被主从复制折磨过的应用开发者。
1. GTID复制从库报错停住的常见原因与排查思路
1.1 GTID复制为什么容不下“报错事务”
GTID(Global Transaction Identifier,全局事务标识符)是MySQL 5.6开始引入、5.7和8.0里已经非常成熟的一套复制机制。它的核心是把每个事务标记成一个全局唯一ID,格式类似server_uuid:transaction_id,比如0a2e3f4a-1b2c-4d5e-8f9a-001122334455:1024。主库上每产生一个事务,就分配一个GTID,从库根据这个GTID来确认自己执行到哪一步。
这和传统的基于binlog文件+Position的复制有一个本质区别:传统复制是按“物理位置”找进度,从库只关心下一个该执行哪个文件、哪个偏移量;GTID复制则是按“逻辑事务ID”对齐进度,从库的Executed_Gtid_Set就是它的执行账本。这个设计让主从切换、故障恢复和级联复制都简单了,但也带来了一个绕不开的毛病:某个GTID一旦应用失败,SQL线程就停在那里,后面的所有事务都在排队等着,直到你把这个卡点解决掉。
可以这么理解:传统复制像是按快递单号逐个签收包裹,遇到地址不对还能跳过去处理下一个;GTID复制则像按流水号记账,某一笔账记错或记不下去,后面的账目必须全部冻结,等对账完才能继续。所以在GTID复制环境下,“跳过报错事务”不是随手一按的事,它相当于在账本里手动补一笔空账,让系统误以为这笔已经处理过了。
1.2 从库SQL线程报错的典型场景
根据我遇到的案例,从库SQL线程停止基本逃不开下面几类原因:
- 从库被外部写入,造成主键冲突。最常见的是应用直连从库做读写,或者某次数据订正脚本跑错了库,在从库插入了和主库重复的数据。
- 主库执行了删除或更新,从库没有对应的行。比如主库删掉了一行数据,但从库因为之前延迟还没来得及删,等真正执行这条DELETE时发现影响行数为0,在严格模式下直接报错。
- 从库表结构不一致。比如主库加了列、改了字符集,从库没同步,复制到相关DML时就可能报
Unknown column或字符集转换错误。 - 外键约束问题。主库按顺序执行事务没问题,但从库因为某些历史数据缺失,删除父表记录时被子表的外键挡住。
- relay log损坏或复制线程异常退出。这类情况相对少见,但一旦出现,报错可能五花八门。
看到报错之后,第一反应不是急着跳,而是先打开SHOW REPLICA STATUS\G(MySQL 8.0.22之前用SHOW SLAVE STATUS\G)记录现场信息。重点看这几个字段:
Replica_SQL_Running: 是否还在运行,报错后通常是No。Last_SQL_Error: 上次SQL线程报错的完整内容。Last_SQL_Error_Timestamp: 报错时间,方便去查硬件、网络或操作记录。Retrieved_Gtid_Set: 已经从主库拉过来的GTID集合。Executed_Gtid_Set: 已经执行成功的GTID集合。
把这几项截图或记录下来,后面不管是跳过还是排查,都有据可查。
2. 跳过报错事务前必须完成的4项安全检查
跳过事务本身只需要几条SQL,真正的风险在于“该不该跳”。我见过不少因为盲目跳过,最后导致主从长期不一致的案例。所以,动手之前先回答下面几个问题。
2.1 确认这类错误是否属于“可跳过”范围
适合跳过的场景,本质是:这个GTID对应的变更已经在从库上生效,或者它根本不需要在从库再次执行。典型情况包括:
- 报错是主键冲突,且冲突的行在从库上已经存在,而主库上的同一条INSERT是第一次插入。这通常说明从库已经有一条相同主键的数据,跳过可以让复制继续。
- 报错是DELETE影响行数为0,且从库上确实没有这条记录,而主库上这条记录已经删掉了。此时跳过不会造成额外差异。
- 手动业务订正时在从库上误操作产生了影响,确认这次误操作就是导致复制冲突的根源。
不适合跳过的场景,更要警惕:
- 报错事务在主库上是一个正常的DELETE,但从库因为延迟还没执行到这条记录,跳过会导致从库残留主库已经不存在的行。
- 报错原因是表结构不一致、缺少字段、存储引擎问题。这种情况下跳过只是把问题掩盖起来,后面的相关事务大概率还会继续报错。
- 报错事务涉及复杂的外键关联,跳过可能让从库数据在后续事务中越来越乱。
怎么判断属于哪一类?最朴素的方案是去从库上直接查数据。比如报错是Duplicate entry '1001' for key 'PRIMARY',那就查一下这个主键在从库是否存在;如果存在,大概率可以跳。如果报错是Can't find record in t_user,就查一下对应记录是否真的不存在。必要时还可以在主库上用mysqlbinlog解析binlog里这个GTID对应的SQL,看看事务到底做了什么。
2.2 定位报错事务对应的GTID
在GTID复制中,SQL线程报错时,MySQL的错误日志通常会明确打印出失败事务的GTID。比如错误日志里会有这样一行:
[ERROR] [MY-010584] Replica SQL for channel '': ... Error 'Duplicate entry ...' on query ... GTID of the failed transaction is 0a2e3f4a-1b2c-4d5e-8f9a-001122334455:45这是最直接的来源。如果错误日志没打印,也可以通过Retrieved_Gtid_Set和Executed_Gtid_Set的差集来推断。因为SQL线程报错后,IO线程通常还在继续拉取新事务,所以Retrieved_Gtid_Set会包含已经拉到relay log但还没执行的GTID,而失败事务恰好是“想执行但没执行成功”的那个。
从MySQL 8.0开始,还能从performance_schema里拿更细的报错信息:
SELECT WORKER_ID, LAST_ERROR_MESSAGE, LAST_ERROR_TIMESTAMP FROM performance_schema.replication_applier_status_by_worker\G这个表记录了每个应用线程最近一次的错误消息,很多情况下比SHOW REPLICA STATUS里的信息更完整。
2.3 备份关键表和记录当前位点
跳过前不一定要全量备份,但至少要记录当前复制位点和涉及报错表的状态。如果是核心业务表,强烈建议先在从库上做一个快速逻辑备份:
mysqldump -uroot -p --single-transaction --set-gtid-purged=OFF appdb t_user > /tmp/t_user_before_skip.sql注意这里--set-gtid-purged=OFF很关键,因为只是做临时备份,不需要导入到其他实例,避免备份文件携带GTID信息造成干扰。
同时记下报错时刻的Executed_Master_Log_Pos和Read_Master_Log_Pos,万一跳过之后发现不对劲,还能知道当前复制回到了哪个位置。
2.4 确认只有SQL线程报错,IO线程状态正常
很多新手在跳过时把主从复制整个停了,其实没有必要。除非IO线程也报错,否则只停SQL线程就行。IO线程继续工作可以让relay log持续积压,SQL线程恢复后能更快追赶进度。
执行前确认一下状态:
SHOW REPLICA STATUS\G只要Replica_IO_Running是Yes,Replica_SQL_Running是No,就可以按照下一节的方法只处理SQL线程。
3. 核心实操:通过空事务注入法跳过指定GTID(命令详解)
这是GTID复制下跳过报错事务的标准操作方法,官方没有提供一条SQL直接“跳过当前错误”,但空事务注入法在业界被广泛验证,核心原理是:手动执行一个不带任何数据变更的BEGIN; COMMIT;空事务,让MySQL把这个空事务记录为某个GTID的执行完成,从而让SQL线程跳过该GTID。
3.1 手动跳过的完整SQL序列
假设报错事务的GTID已经确定为0a2e3f4a-1b2c-4d5e-8f9a-001122334455:45,完整的跳过操作如下:
-- 1. 只停止SQL线程,IO线程保持运行 STOP REPLICA SQL_THREAD; -- 2. 指定下一个事务的GTID为报错的GTID SET gtid_next = '0a2e3f4a-1b2c-4d5e-8f9a-001122334455:45'; -- 3. 执行一个空事务,不修改任何数据,但会占用这个GTID BEGIN; COMMIT; -- 4. 恢复gtid_next为自动分配模式 SET gtid_next = 'AUTOMATIC'; -- 5. 启动SQL线程 START REPLICA SQL_THREAD;执行完最后一条命令后,再执行一次SHOW REPLICA STATUS\G,正常情况下Last_SQL_Error会清空,Replica_SQL_Running变为Yes,Seconds_Behind_Master开始逐渐缩小。
3.2 为什么空事务能“骗过”复制链路
GTID复制在判断一个事务是否已经执行时,依据是Executed_Gtid_Set中是否包含该GTID。我们手动把gtid_next设置成报错事务的GTID,再执行一条空事务,MySQL就会把这个GTID写入从库自身的事务记录中。虽然空事务没有产生任何数据变更,但在复制账本里,这个GTID已经被标记为“已完成”。
SQL线程恢复后,读取下一个事务时发现该GTID已经存在于自己的执行记录里,就会跳过它,继续处理后面的GTID。这就相当于在一本流水账里补了一条无实际金额的过账记录,让账目继续往下记。
注意:跳过不是修数据。如果这个事务本该在从库产生数据变更,但被空事务占位了,从库就会永久缺少这次变更。所以前面反复强调,一定要确认该事务的数据变更确实不需要执行。
3.3 传统跳过命令为什么在GTID模式下失效
很多用过传统复制的人会想到sql_slave_skip_counter,在GTID模式下这条路已经堵死了。如果执行:
STOP REPLICA SQL_THREAD; SET GLOBAL sql_slave_skip_counter = 1; START REPLICA SQL_THREAD;MySQL会直接报错:
ERROR 1858 (HY000): sql_slave_skip_counter is not supported when GTID_MODE is ON.原因在于GTID模式不允许凭空跳过事务序号,因为每个GTID都必须有明确归属。空事务注入法相当于强行给这条事务“补票”,所以它是GTID复制环境下更合规的方案。
3.4 最容易踩的3个细节坑
第一个坑:忘记恢复gtid_next = 'AUTOMATIC'。如果跳过的会话继续执行其他事务,后面所有显式事务都可能被分配成同一个GTID,直接导致复制链路上出现重复GTID,报错很难排查。所以第4步一定不能省。
第二个坑:跳错GTID。如果gtid_next设置成了其他事务的编号,SQL线程恢复后会继续卡在原来的位置,甚至可能把下一条正常事务的空GTID记录到执行集合里,导致真正的正常事务被跳过。建议在设置之前反复比对复制状态里的报错事务号。
第三个坑:级联复制下只跳了当前从库。如果从库本身还在向下游传递binlog,并且开启了log_slave_updates,下游从库可能同样卡在这个GTID上,需要逐台确认和处理。
4. 批量跳过多个报错GTID的脚本方案
如果从库积压了大量报错事务,一个个手工操作效率太低。这时候可以做一个小脚本,自动提取SQL线程报错对应的GTID,循环执行空事务注入。但脚本必须加上充分的安全确认,不能拿到什么跳什么。
4.1 从复制状态中自动提取报错GTID
一个比较稳的提取方式是查询performance_schema.replication_applier_status_by_worker的LAST_ERROR_MESSAGE,从报错文本里用正则把UUID和事务号抠出来。比如:
mysql -uroot -p -N -e "SELECT LAST_ERROR_MESSAGE FROM performance_schema.replication_applier_status_by_worker\G" | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[0-9]+' | head -1如果没有performance_schema权限,也可以从SHOW REPLICA STATUS\G的Last_SQL_Error里提取,只是匹配字段要多一步。
4.2 循环跳过的参考脚本
下面这个脚本只能用在已经确认“错误类型一致且可安全跳过”的场景里,比如从库被外部批量写入了重复数据,导致连续多个INSERT都报主键冲突。
#!/bin/bash # 自动跳过从库SQL线程报错的GTID # 使用前提:只处理主键冲突/记录缺失等已确认的安全错误类型 MYSQL="mysql -uroot -pYourPass -h127.0.0.1" for i in $(seq 1 50); do ERR_GTID=$($MYSQL -N -e "SHOW REPLICA STATUS\G" | grep 'Last_SQL_Error' | grep -oE '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}:[0-9]+' | head -1) if [ -z "$ERR_GTID" ]; then echo "第 $i 轮:未发现报错GTID,复制可能已恢复。" break fi echo "第 $i 轮:跳过 $ERR_GTID" $MYSQL -e "STOP REPLICA SQL_THREAD; SET gtid_next='$ERR_GTID'; BEGIN; COMMIT; SET gtid_next='AUTOMATIC'; START REPLICA SQL_THREAD;" sleep 1 done # 最终状态检查 $MYSQL -e "SHOW REPLICA STATUS\G" | grep -E 'Replica_SQL_Running|Last_SQL_Error|Seconds_Behind_Master'这个脚本最多执行50轮,如果一轮跳过后又卡在下一个GTID,会自动继续处理。脚本跑完后必须人工检查Seconds_Behind_Master是否在减小,以及主从数据是否一致。
重要提醒:任何自动跳过脚本都必须在明确理解报错原因后使用。如果从库和主库表结构不一致、缺失索引、字符集不同,跳过再多轮也解决不了根本问题,只会把不一致越堆越大。
5. 跳过事务后的数据一致性校验与修复
跳过只是让复制链路恢复流动,真正的收尾工作是确认主从数据是否一致。否则,下一个深夜报警会更难处理。
5.1 快速行数与校验值对比
对于报错涉及的表,先在主库和从库分别执行:
SELECT COUNT(*) FROM appdb.t_user; SELECT CHECKSUM TABLE appdb.t_user;CHECKSUM TABLE返回的校验值如果一致,至少能说明这张表当前数据是相同的。但要注意,CHECKSUM TABLE会扫描整张表,大表会产生一定压力,建议放在低峰期执行。
如果只是想快速抽样,可以对核心业务表按主键ID范围分段对比:
-- 主库和从库分别执行 SELECT COUNT(*) FROM appdb.t_order WHERE id BETWEEN 1 AND 100000;5.2 使用校验工具做全量对比
单表简单场景用SQL手工对比问题不大,如果表多、数据量大,建议用专业的复制校验工具。目前常用的开源方案是pt-table-checksum,它的思路是在主库上按块计算校验和,分批发送到从库比对,输出不一致的表和行范围。
pt-table-checksum h=主库地址,u=root,p=密码 --databases=appdb --tables=t_user --replicate=percona.checksums执行结束后查看结果表:
SELECT db, tbl, SUM(this_cnt) AS total_rows, COUNT(*) AS chunks FROM percona.checksums WHERE master_cnt <> this_cnt OR master_crc <> this_crc GROUP BY db, tbl;如果查出差异,再针对具体表修复。
5.3 常见不一致的修复思路
如果差异不大,可以直接在从库上修正。比如从库多出主键冲突的行,确认该行在主库不存在后删除:
DELETE FROM appdb.t_user WHERE id = 1001;如果是从库缺失某些行,可以从主库导出这些行的数据补插。但补插时要注意GTID复制的约束,导出的SQL里不要带GTID信息,否则可能影响从库的GTID执行记录。比较好的做法是用mysqldump --set-gtid-purged=OFF导出指定表,然后导入从库。
如果差异范围很大,比如多张表、多个库都对不上,最省事的方案是重建从库。用备份工具做全量备份后恢复到从库,重新配置复制链路。不要试图一个事务一个事务地修补,那样既慢又不安全。
5.4 重建从库时的GTID复制注意事项
重建从库听起来简单,但GTID环境下有个细节容易忽略:备份文件需要保留GTID信息,才能让新从库知道从哪个位置开始追。使用mysqldump做备份时,不要关闭--set-gtid-purged,这样导入新从库后,它会自动从正确的GTID位置开始追主库。而如果只是想修复单表,导出的文件反而要加上--set-gtid-purged=OFF,两者目的完全不同,千万别记混。
6. 从库跳过事务常见问题排查实录
6.1 常见错误与解决方案速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 执行空事务后仍然报同一个GTID | gtid_next没有设置成功,或者空事务未提交 | 重新执行完整跳过流程,检查每个步骤是否成功 |
设置gtid_next后出现状态异常 | 当前会话还有未提交的事务 | 执行ROLLBACK; SET gtid_next='AUTOMATIC';恢复 |
跳过几个GTID后Seconds_Behind_Master为NULL | SQL线程刚启动,正在追赶relay log | 等待几秒后重新查看状态 |
| 跳过后续连接续报下一个GTID错误 | 从库存在多个冲突事务 | 确认错误类型一致后,用批量脚本循环跳过 |
| 从库复制恢复,但业务查询出的数据与主库不同 | 跳过的事务导致从库缺失数据变更 | 使用校验工具定位差异表,再针对性修复或重建从库 |
SHOW SLAVE STATUS命令报语法错误 | MySQL 8.0.22后命令更名 | 改用SHOW REPLICA STATUS |
6.2 半月后才发现数据不一致,问题出在哪
一个典型的反面案例:某天凌晨从库报主键冲突,值班同学确认冲突的行双方都有,于是跳过。半个月后业务侧发现某个报表数据对不上,一查发现当天其实有连续三个报错事务,第一个是主键冲突,第二个和第三个分别是对同一行数据的更新和删除。值班同学只跳了第一个,复制接着卡在第二个,他又跳了一次,连续跳完后这三个GTID里包含的数据变更被整体跳过,从库缺了一段关键数据。
这个案例说明两个道理:第一,跳过操作不能只看当前报错,要结合主库binlog把一连串事务看全;第二,跳完之后必须做一致性校验,不能看到Seconds_Behind_Master归零就认为万事大吉。
6.3 如何减少从库报错的日常习惯
与其每次花半小时处理复制卡死,不如在架构层面把风险降下来。
- 给从库开启
read_only,业务账号不要授予写权限,至少也要开super_read_only,防止管理员以外的人直连从库写入。 - 在监控系统里增加对
Last_SQL_Error和Replica_SQL_Running的告警,让复制异常在几分钟内被发现,而不是业务抗议时才知道。 - 定期对比主从表结构,避免核心表结构不一致拖很久。
- 大事务操作前评估复制延迟,必要时拆批执行,降低单个GTID内部失败对链路的冲击。
- 主库和从库的SQL模式尽量保持一致,尤其是
STRICT_TRANS_TABLES和sql_mode相关配置。
6.4 我处理这类问题的一点心得
在生产环境里操作跳过事务,我习惯把流程固定成四步:先记录复制状态和错误日志,再确认GTID对应事务在从库上的实际影响,然后只用SQL线程级别的跳过方式,最后无论跳过多少都要做数据校验。这套流程看着保守,但它确实帮我避开过好几次更大的事故。
另外,如果错误信息里出现外键约束失败、字段不存在这类结构性错误,我基本不会考虑跳过,而是直接走重建从库或修复表结构的流程。结构性问题和数据重复不同,它不是靠一两个空事务能掩盖的。
最后再说一个细节:跳过事务时用的会话,执行完START REPLICA SQL_THREAD后不要立刻关闭,可以先观察几秒状态;但gtid_next一定要在同一会话里恢复成AUTOMATIC,跨会话设置是不生效的。这个细节看起来不起眼,遗漏一次就够你折腾一阵子。