
1. GTID 复制里的事务报错到底是怎么发生的MySQL GTID 复制环境下从库 SQL 线程因为某一个事务报错停在原地这是我在生产环境里见过最多的一种复制故障。很多朋友第一反应是“把坏事务跳过”但真正的问题是GTID 复制不像传统的位点复制那样可以简单跳过事件跳得不对主从之间会留下一个永远补不上的洞甚至让自动定位反复卡在同一个事务上。这篇文章从原理和实操两个层面把“设置从库跳过一个报错的事务”这个动作拆开讲希望能帮你少走弯路。1.1 先回顾一下 GTID 复制和普通复制的本质区别GTID 全称是 Global Transaction Identifier也就是全局事务标识符。它在主库给每一个提交事务分配一个唯一编号通常长这样6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205前面是主库的 server_uuid冒号后面是事务序号。从库在应用这个事务之后会把这个 GTID 写进自己的gtid_executed集合里。自动定位模式下从库连接主库时会把自己已经执行的 GTID 集合发给主库主库一看就知道还差哪些事务于是只补充差额。这一点和传统按 binlog 文件加偏移量的复制模型完全不同。老式复制里从库只需要告诉主库“我要从哪个日志文件的哪个位置继续读”中间是否漏掉事务、是否重复执行全靠从库自身的执行状态来兜底。但 GTID 复制要求的是“事务级账本”每个事务只能被执行一次而且必须被记录一次。要不要跳过、怎么跳过都必须围绕“GTID 集合有没有缺口”来讨论。打个比方快递公司每个包裹都有唯一单号中转站收到包裹后要在系统里登记“已签收”后续流程才继续处理新包裹。如果中转站把一个坏包裹直接扔掉却没有在系统里登记这个单号总部就会反复发送同一包裹整个流水线都会堵住。GTID 复制里的坏事务就对应一个唯一单号你必须在系统里“登记”它已经被处理业务层的数据变更才能不存在。这正是后面所有跳过方法的底层逻辑。1.2 报错事务是怎么“卡住”复制线程的主从复制的链路其实很简单IO 线程从主库拉取 binlog写入从库的中继日志SQL 线程再读取中继日志事件在自己的库上执行。整个过程里如果 SQL 线程执行到某一条 SQL 出错它默认会停止后续所有应用动作把错误记录在Last_SQL_Error和Last_SQL_Errno里。此时IO 线程很可能还在继续拉日志中继日志不断累积但 SQL 线程就是不动Seconds_Behind_Master会越拉越大。这个“卡住”的报错事务通常是当时正在执行的那一个 GTID。它可能只是一个大事务中的中间语句出了问题也可能是业务某个简单 DML 碰到了约束冲突。常见报错原因包括主从数据从一开始就不完全一致从库上已经有同主键值主库再次插入或者更新出现 duplicate entry从库上原本存在一些业务直接写入的“幽灵数据”和主库 binlog 重放的数据撞车外键约束、字符集转换、表结构不一致等也会引发中断。最坑的是出错之后不会自动重试也不会自动跳过。你手动介入之前SQL 线程就一直停在那里后面一堆正常事务全部被堵住。很多人一看Slave_SQL_Running: No就急着跳过但对当前故障事务的来龙去脉没有完整判断结果跳完之后复制继续跑过了几分钟又卡在下一个事务上。1.3 为什么传统跳事务方法在 GTID 模式下常常失效传统位点复制时代从库报错后最简单的处理方式就是STOP SLAVE; SET GLOBAL SQL_SLAVE_SKIP_COUNTER1; START SLAVE;。这个变量的意思是让 SQL 线程在执行时跳过接下来指定数量的事件不执行具体数据变更但线程继续往后走。在只看日志文件和偏移量的复制模型里操作者和复制引擎都认为“位置往前走”问题似乎就解决了。但 GTID 复制中事务的唯一标识并不会因为“跳过事件”就消失。即使你在从库临时跳过了当前这条报错 SQL这个事务的 GTID 依然不在从库的gtid_executed集合里。下次自动定位时主库发现从库还有未执行事务就会再次把这个事务发给从库从库执行时又遇到同样的错误又停下来。一些旧版本里虽然可能暂时不报错或者复制线程能往前跑一段但 GTID 集合的缺口早晚会在日志清理、主从切换、重建复制关系时暴露出来。所以 GTID 模式下的正确思路不是“假装没看见”而是“让系统以为这个事务已经在副本上执行过”。也就是把缺失的 GTID 补进从库的已执行集合但又不真的重放那条坏 SQL。接下来的操作全都围绕这个核心目标展开。2. 动手之前必须做的三项检查别上来就跳过跳过报错事务虽然不难但前提是你得知道坏事务到底是哪一个。这一步如果做错后面所有操作都是白费。很多人在SHOW SLAVE STATUS里看不到 GTID就以为找不到其实只是没找对地方。2.1 看状态信息SHOW SLAVE STATUS / SHOW REPLICA STATUS先登录从库执行SHOW SLAVE STATUS\GMySQL 8.0 里也可以用SHOW REPLICA STATUS\G两个命令输出结构基本一致只是关键词从 Slave 换成了 Replica。重点关注几个字段Replica_IO_RunningIO 线程是否正常。Replica_SQL_RunningSQL 线程是否正常报错时通常是 No。Last_SQL_Errno和Last_SQL_Error最近一次 SQL 线程错误的编号和内容。Retrieved_Gtid_SetIO 线程已经从中继日志里拿到的 GTID 集合。Executed_Gtid_SetSQL 线程已经成功执行的 GTID 集合。Auto_Position是否为 1确认是否处于 GTID 自动定位模式。Master_UUID主库的 server_uuid这个在拼 GTID 时会用到。Last_SQL_Error一般会直接给出报错的 SQL 和错误原因但它不一定包含完整的 GTID。比如它会写Duplicate entry 1001 for key PRIMARY但不会告诉你这个插入语句属于哪个事务。因此光靠这条信息还不够需要继续往下挖。2.2 从错误日志和性能表里挖出具体 GTIDMySQL 错误日志里通常会有更详细的记录。在报错事务被 SQL 线程执行失败时mysqld 会把事务 GTID 一并写进日志日志行类似[ERROR] [MY-010587] Replica SQL for channel : Worker ... failed executing transaction 6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205 at source log mysql-bin.000012, end_log_pos 2345这里面的6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205就是要找的那个 GTID。如果你管理的实例启用了多线程复制也就是 MTS可以从performance_schema里查语句类似SELECT WORKER_ID, LAST_ERROR_NUMBER, LAST_ERROR_MESSAGE, LAST_APPLYING_TRANSACTION, LAST_APPLIED_TRANSACTION, LAST_ERROR_TIMESTAMP FROM performance_schema.replication_applier_status_by_worker\G找到有LAST_ERROR_MESSAGE的那一行LAST_APPLYING_TRANSACTION一般就是报错事务的 GTID。不同 MySQL 版本里字段略有差异但思路一致错误日志和性能表比状态变量更快、更直接。拿到完整 GTID 之后还要到从库上确认一下SHOW GLOBAL VARIABLES LIKE gtid_executed;看看这个 GTID 是不是已经存在。如果已经存在说明并不是这个事务缺号问题大概率出在别处比如还有其他更早的事务没执行完。2.3 用一份小清单判断该跳还是不该跳跳过只是止损不是治病。该不该跳得看错误类型和业务影响。下面是这些年我常用的判断思路错误类型是否适合跳过备注duplicate key、唯一键冲突可以但要先评估主从数据差异可能需要清理从库上多余的历史数据update/delete 找不到行可以止损但业务影响要确认如果后续事务依赖被删行还会继续报错表结构不一致、列不存在不建议跳过跳过当前事务后续同类事务还是会崩外键约束、字符集冲突谨慎属于结构性问题应优先修复结构DDL 失败不要跳过会造成表结构永久性不一致判断逻辑其实很简单如果报错属于偶发性、单次性比如主库某个事务在从库撞了唯一键事务本身对业务数据影响不大那么跳过去是合理的。如果一天之内已经跳了好几次基本可以判断主从数据存在区域性或结构性问题必须先做数据比对而不是继续一个一个跳。3. 推荐做法注入空事务让缺失的 GTID 补位这是目前最主流的做法也是官方文档和很多 DBA 都在用的方案。原理说起来只有一句话把故障事务的 GTID 手动指定给一个空事务让从库认为这个事务已经执行过了。3.1 空事务跳过的原理在 MySQL 会话里gtid_next这个会话级变量决定下一个事务应该使用哪个 GTID。正常情况下它是AUTOMATIC表示由系统自动分配。我们可以把它改成一个具体的值然后执行一个不包含任何数据变更的事务例如BEGIN; COMMIT;。这样一个“空事务”就会被生成并且记录到从库的gtid_executed集合里。因为空事务本身不会修改任何业务数据它不会引入额外的主从差异但它在 GTID 账本里补上了缺号。主库下一次和从库做自动定位时发现这个事务已经在从库执行过就不会再次推送它了。SQL 线程随后会继续应用中继日志里后面的正常事务。需要注意空事务和真实执行的坏事务是两回事。如果你跳过的是一个插入操作空事务不会替你插入数据跳过的是一个删除操作空事务也不会删除数据。它只是“跳过”了不是“重放”。这一点想不清楚后面很容易被数据不一致反噬。3.2 一条一条跟着做的跳过程序推荐使用独立会话按下面的顺序执行。命令本身不复杂但顺序不要乱。-- 1. 停止整个复制线程更稳妥 STOP SLAVE; -- 2. 再次确认要跳过的 GTID -- 假设从错误日志里拿到的是 6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205 SHOW SLAVE STATUS\G -- 3. 指定会话级 gtid_next 为要跳过的 GTID SET SESSION.gtid_next 6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205; -- 4. 开启一个事务并提交生成空事务 BEGIN; COMMIT; -- 5. 恢复自动模式 SET SESSION.gtid_next AUTOMATIC; -- 6. 重新启动复制线程 START SLAVE;这里有一个细节你先用了STOP SLAVE后面就要用START SLAVE不要一会停整个线程一会又只启动 SQL 线程。如果只停了 SQL 线程启动时就用START SLAVE SQL_THREAD。两类写法都能跑通但建议每次都写清楚避免在出问题时还要猜当时停的是哪个线程。执行第 3 步之前要确保当前会话没有正在进行的事务。如果在已经开启事务的会话里设置gtid_next会报错。第 4 步的BEGIN; COMMIT;是关键只设置gtid_next不提交空事务不会真正生效。第 5 步必须把gtid_next恢复到AUTOMATIC否则后续执行的任何事务都会尝试使用同一个固定 GTID可能造成新的冲突。3.3 执行后的健康检查跳完之后不能转身就走至少要看三件事。第一SHOW SLAVE STATUS\G里的Replica_SQL_Running应该变成YesLast_SQL_Errno应该恢复成 0。第二从库的gtid_executed集合里应该能看到刚才跳过的 GTID。第三观察一段时间确认Seconds_Behind_Master在减小而不是再次增长。如果执行过程中报错最常见的无非两类报错说gtid_next设置不了比如当前会话还持有事务或者复制线程没有真正停止。解决方法是先结束当前事务或者确认STOP SLAVE已经生效。报错说 GTID 已经存在也就是重复设置。这种情况说明该 GTID 本来就在执行集合里不用跳过应该去查为什么复制还停着。还有一种容易被忽略的情况如果你跳过的 GTID 并不是复制线程当前正在卡住的那一个命令可能也能执行成功但复制线程依然停在原地。所以一定先确认 GTID 来源别凭感觉拼一个。4. 特殊情况下的其他选择改 gtid_purged 和重建从库单个报错事务用空事务跳是最清楚的。但如果从库因为各种历史原因落后太多要跳过一批事务或者当前事务已经被跳烂了你可能需要考虑其他手段。不过这些手段更重使用之前必须想清楚后果。4.1 想把一批 GTID 统一标记为已执行怎么办如果要从库放弃一大批历史事务一种方式是重置复制环境然后直接通过SET GLOBAL gtid_purged把一段 GTID 范围注入。有人会把gtid_purged和gtid_executed搞混后者是已经执行过的事务集合前者是已经被清理掉但不再执行的事务集合。设置gtid_purged可以间接影响从库认为哪些事务已经处理完了。一个大致的操作轮廓是RESET MASTER; SET GLOBAL gtid_purged 源库UUID:开始序号-结束序号; CHANGE MASTER TO MASTER_AUTO_POSITION1, MASTER_HOST..., MASTER_USER..., MASTER_PASSWORD...; START SLAVE;这套东西通常用在从库重新初始化的时候。RESET MASTER会清空从库自己的 binlog对运行中的环境影响很大必须经过严格评估并且要保证跳过的那些事务对应的数据确实可以从主库重新全量同步或者业务上已经允许暂时不一致。普通生产环境里我一般不建议为了省事用这个方法去处理几个报错事务风险远比收益大。4.2 为什么“跳过事务”不能替代数据修复跳过事务之后从库的 GTID 集合虽然补齐了但业务数据的最终状态可能和主库不一致。举个例子主库删除了一行记录从库因为某些原因没找到该行导致报错你用空事务把这个删除事务标记成已执行。从库上那条本来应该被删掉的数据还在之后主库如果又插入一条同主键的新记录从库大概率又会因为唯一键冲突继续报错。所以跳空事务只是让复制线程先动起来不等于数据已经修好。跳过之后要安排一次数据校验至少要对涉及的那几张表做行数和主键值比对。如果发现差异可以根据差异大小选择手动补数、局部重建甚至直接重建整个从库。想靠一次次跳过掩盖数据问题只会让从库越来越不可信。4.3 多从库环境里容易忽略的统一性问题如果你维护的复制环境下有多个从库不要觉得主库只有一个从库各自处理就行。多个从库可能同时从同一个主库拉取日志也同时因为相同的数据问题报错。如果只修了一个从库其他从库还在反复请求同一个坏事务主库压力会增大日志场景也会更乱。更好的做法是先在主库或者管理机上统一收集各个从库的报错 GTID确认它们卡在同一个事务之后再分别到各个从库上执行同一条空事务跳过命令。跳完之后也要统一核对每个从库的Executed_Gtid_Set和线程状态。如果发现某个从库多跳了、漏跳了要立刻修正否则后续主从切换时很容易选出数据不一致的从库。5. 完整实录一次 duplicate entry 报错的处理全过程理论知识说再多不如完整走一遍流程。下面用一个简化场景演示主库向某业务表插入一条记录从库因为历史数据多了一条同主键的记录SQL 线程卡死。我们把它找出来、跳过去再确认后续复制状态。5.1 故障现象与现场信息假设主库执行了INSERT INTO user_info (id, name) VALUES (1001, old_duplicate);但从库的user_info表里已经存在一条id 1001的记录于是从库 SQL 线程报错。执行SHOW SLAVE STATUS\G能看到类似输出Slave_IO_Running: Yes Slave_SQL_Running: No Last_SQL_Errno: 1062 Last_SQL_Error: Error Duplicate entry 1001 for key PRIMARY on query. Default database: app. Query: insert into user_info (id, name) values (1001, old_duplicate)然后去从库错误日志里找发现里面有这一行[ERROR] [MY-010587] Replica SQL for channel : Worker ... failed executing transaction 6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205到这里已经确认要跳过的 GTID 是6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205。5.2 跳过的完整命令序列在从库执行以下命令STOP SLAVE; SET SESSION.gtid_next 6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205; BEGIN; COMMIT; SET SESSION.gtid_next AUTOMATIC; START SLAVE;执行过程中没有报错。然后查看复制状态Slave_IO_Running: Yes Slave_SQL_Running: Yes Last_SQL_Errno: 0 Last_SQL_Error:再查一下 GTID 集合SHOW GLOBAL VARIABLES LIKE gtid_executed;可以看到刚才的6ee0d1e6-8f49-11ec-a7e1-fa163e123456:1205已经在集合里。这说明空事务注入成功复制中断被解除了。5.3 跳过后必须做的一致性复核这一步很多人会漏掉。刚才跳过的插入事务并没有真的把id1001的数据插进从库从库上还留着一条可能和主库不一致的旧记录。如果业务要求从库数据完全一致这里就不能算是处理结束。最简单的做法是先在主库和从库分别查询目标数据确认差异。例如SELECT id, name FROM app.user_info WHERE id 1001;如果主库返回的是正确数据从库返回的是旧数据那就需要决定是删除从库的旧数据还是把从库重建。删除旧数据之后主库会不会再次推送同一条插入事务不会因为那个 GTID 已经被跳过了主库只推送未执行的事务。所以光删旧数据还不够你还得手动把主库的正确数据补到从库或者直接重建从库。在真实生产环境里如果错误事务只涉及几条业务数据我通常会评估差异后手动补数如果涉及范围较大直接重建这个从库反而更省心。重建方式可以是从主库做全量备份后在从库恢复或者用现有备份重建后追日志。重点是跳过不能让从库自动变得和主库一致它只是给了你一个继续操作的机会。6. 这些年我踩过的坑都帮你整理好了跳过操作本身不难但在真实环境里出问题往往出在一些细节上。下面这些坑我都见过有的还是自己踩过的。6.1 这些细节最容易翻车第一没有确认gtid_executed里是否已经有目标 GTID。如果你对已经执行的 GTID 再次设置gtid_next会得到重复事务错误。此时不是要跳过而是要去找出真正没执行的缺口在哪。第二设置了gtid_next后执行了ROLLBACK而不是COMMIT。只要事务被回滚空事务就没有真正生成跳过的动作自然不成立。很多人执行完没注意结果就急着启动复制后面发现还是老错误一脸懵。第三跳完忘了把gtid_next恢复成AUTOMATIC。这不一定会立刻报错但当前会话后续一旦执行事务都会带着一个固定 GTID可能出现不可预期的问题。建议养成习惯执行完跳过命令后立刻查一下会话变量。第四在多线程复制环境里看错了 worker 信息。performance_schema.replication_applier_status_by_worker会返回多行每行对应一个 worker。如果只扫一眼第一行可能抓到的不是真正报错的那个 worker。要以LAST_ERROR_MESSAGE非空为准同时参考LAST_ERROR_TIMESTAMP。第五遇到多个复制通道时没有指定 channel。比如你配置了多个源执行STOP SLAVE可能只停默认通道报错的那个通道还在继续。正确做法是带FOR CHANNEL xxx语法操作例如STOP SLAVE FOR CHANNEL xxx;。6.2 一条更稳妥的跳前检查顺序我给自己定的标准流程是先记录现场再评估影响然后备份相关信息最后才执行跳过。具体顺序可以这样用SHOW SLAVE STATUS\G记录当前状态保存错误日志片段和故障事务 GTID。去主库确认这个事务对应的业务操作是什么是否影响核心表。有条件的话先备份故障事务涉及的数据行方便后面比对和恢复。停止复制线程执行空事务注入。启动复制观察至少 5 分钟确认Last_SQL_Errno不反弹。安排一次针对受影响表的数据校验写入运维记录。我个人在实际处理中体会最深的一点是跳空事务是复制中断后的“急救药”不是“根治药”。它解决的是 GTID 缺号导致的复制卡死而不是业务数据差异。只要把这个边界想清楚设置从库跳过一个报错的事务这件事就一点都不悬了。后续再遇到类似问题你也能很快判断出来这个事务到底该跳还是该先修数据。