资讯动态

MySQL主从复制Duplicate entry错误原因与处理方案

发布时间:2026/9/13 6:21:52 来源:尧图企业网站定制
这个报错只要是亲手维护过MySQL主从复制的朋友几乎都撞见过。半夜被监控吵醒打开日志一看[ERROR] Slave SQL for channel : Could not execute Write_rows event on table xxx.xxx; Duplicate entry xxx for key PRIMARY主从同步中断从库落后主库越来越多。第一次遇到确实容易慌但其实这个问题的链路非常清晰处理方式也有成熟套路。这篇文章我把自己踩过的坑、用过的方案、以及为什么某些做法不能乱用一次性讲透。先说明一点这里讨论的报错场景适用于MySQL传统主从复制异步复制、半同步复制以及部分使用通道channel的多源复制环境。核心问题在于从库的SQL线程在回放主库binlog中的写入事件时发现目标表的主键冲突导致事务无法执行SQL线程停止。下面从报错本身的含义开始拆。1. 错误场景还原这个报错到底在说什么1.1 拆解报错关键字很多刚接触主从同步的同学看到一长串英文就先慌了其实逐词拆开就不难懂。Slave SQL for channel 说明这是从库Slave的SQL线程报错。channel 表示默认复制通道如果是多源复制这里会显示具体通道名比如channel master1。多源复制场景下同一个从库可能从多个主库拉取binlog通道就是区分不同复制链路的标识。Could not execute Write_rows eventSQL线程正在执行一个写入行的事件。Write_rows_event是binlog中记录行变更的事件类型之一对应的是INSERT操作。行级复制binlog_formatROW下主库的每一次插入都会在binlog中变成这样一个事件从库拿到后重放到本地表。on table xxx.xxx哪个库哪张表出错了这里会明确给出库名和表名比如testdb.user。Duplicate entry xxx for key PRIMARY这是真正的失败原因翻译成人话就是——从库上这张表里已经有主键值为xxx的行了你再插一条同样的主键唯一性约束不允许。所以整条报错的完整含义是从库在回放主库binlog里的一条INSERT操作时发现目标表中已经存在相同主键的数据插入失败SQL线程停止主从同步中断。这个错之所以经典是因为它几乎是从库数据不一致的最典型信号。1.2 为什么从库会写入失败主库和从库各自维护一份数据正常情况下主库写入什么从库就照着写什么。从库报Duplicate entry只说明一个事实从库上已经有这条数据了但主库的binlog里又出现了一次相同主键的写入。最常见的三种成因从库被手动写入过数据。开发同学连错库在从库上执行了一条INSERT或UPDATE恰好这条数据的主键又落在主库后续binlog的写入范围内。于是主库正常插入从库重放时主键撞车。复制中断后手动跳过事务导致的数据错位。之前主从同步就报过错DBA为了临时顶上业务执行了set global sql_slave_skip_counter1跳过了一个或多个事务。跳过的这些事务里可能包含INSERT也可能包含DELETE和UPDATE跳完之后数据已经和主库对不上了后续新进来的写入事件就可能在从库撞车。备份恢复或重建从库时数据没对齐。比如用某个旧备份搭从库备份里的数据本身就和主库不一致或者恢复备份后binlog位点GTID集合指错了位置从库把主库之前已经执行过的一部分事务又重放了一遍。这三种成因对应的处理思路完全不同所以别急着动手修第一步是先搞清楚为什么会重复。2. 修复前的三步检查先别急着改数据2.1 确认主从状态与错误位置看到报错后第一件事不是直接跳过错误而是完整确认当前复制链路的状态。在从库上执行SHOW SLAVE STATUS\G重点关注几个字段Slave_IO_RunningIO线程是否正常正常情况下是Yes。如果IO线程也断了说明网络或主库binlog推送有问题那是另一条排查线。Slave_SQL_RunningSQL线程是否正常报错场景下通常是No。Last_SQL_Error和Last_SQL_Error_Timestamp记录SQL线程最后一次报错的详细信息就是我们看到的Duplicate entry错误。Exec_Master_Log_Pos和Read_Master_Log_PosSQL线程执行到的位点以及IO线程拉取到的位点。两者差值越大说明从库落后越多。Retrieved_Gtid_Set和Executed_Gtid_SetGTID模式下记录已经拉取和已经执行的事务集合对比能看出具体卡在哪个事务上。Seconds_Behind_Master从库落后主库的秒数这个字段经常是0但SQL线程停止后它会持续变大或显示NULL只能作为参考不能完全依赖。同时用SHOW PROCESSLIST确认有没有长事务在占用表锁避免后续操作被卡住。再用SELECT DATABASE()配合报错里的表名定位具体是哪个表。2.2 判断这条重复数据从哪里来这是整个排查里面最需要经验的一步。不要急着删从库上的多余数据也不要急着跳过事务先回答一个问题从库上那条冲突的数据到底该不该存在在从库上执行SELECT * FROM xxx.xxx WHERE id 冲突的主键值;再回到主库上查同样的记录SELECT * FROM xxx.xxx WHERE id 冲突的主键值;比较一下两边这条记录的内容。这里会出现几种情况主库有这条数据从库也有且内容一致。说明从库之前通过某种方式已经执行过这条INSERT了但现在binlog里又来了一次。典型场景就是数据重复导入或者之前跳过事务时没有跳干净事务被重放了一部分。这种情况适合确认后跳过当前事务。主库有这条数据从库也有但内容不一样。说明从库上的这条数据是人为写入或历史遗留的脏数据与主库不一致。直接跳过事务会让两边内容持续不一致后续更新这条数据时还会继续报错。这种情况需要先修正从库数据或者在确认业务可接受的前提下删除从库上的冲突行再继续复制。主库没有这条数据但从库有。说明从库多出了一条主库不存在的垃圾数据大概率是某次人为操作或数据恢复不当导致。这种情况要先删除从库上多余的冲突行再继续复制否则即使跳过了当前这个事务后续任何涉及该主键的写操作都会继续报错。这三个判断方向对应完全不同的操作切记先查清楚再动手。我见过太多一上来就sql_slave_skip_counter1跳过错误的操作结果跳出一个数据长期不一致的坑后续每几天就报一次错天天半夜被叫起来处理。2.3 备份现场给自己留后路在动手修改之前无论采用哪种修复方案都建议在从库上做一次备份即使只是单表也要做。别嫌麻烦有一次我跳过一个事务后才发现那个事务里还包含一个隐性的DDL变更导致从库上一个字段的定义和主库不一致最后只能重新搭库。如果有备份回滚成本会低很多。对于单表数据最轻量的方式是mysqldump -uusername -p --set-gtid-purgedOFF --single-transaction --skip-lock-tables xxx xxx /tmp/backup_xxx_$(date %F).sql注意--set-gtid-purgedOFF这个参数备份从库数据时如果不加mysqldump可能把GTID信息也导出来恢复时会污染GTID集合。除非你有意做全量备份并清空GTID否则在从库上做单表备份务必加上这个参数。如果数据量很大也可以用CREATE TABLE xxx_bak AS SELECT * FROM xxx;的方式在本地快速复制一张表作为操作前的临时快照。这一步不是必须的但能让你后续操作时心里有底。3. 主流修复方案与适用场景对比3.1 方案一确认后跳过事务传统位点模式这是最常用的应急方案适用于确实已经确认从库数据与主库最终一致只是binlog里的事务重复执行的场景。操作只有三步STOP SLAVE SQL_THREAD; SET GLOBAL sql_slave_skip_counter 1; START SLAVE SQL_THREAD;在GTID模式下sql_slave_skip_counter已经废弃执行会报错所以先确认当前的复制模式。在传统位点模式下sql_slave_skip_counter 1表示跳过SQL线程接下来要执行的下一个事件注意是事件而不是事务。这里有个关键细节一个事务在binlog里可能包含多个事件。以ROW格式为例一个事务包含GTID_LOG_EVENT、QUERY_EVENT事务开始、多个WRITE_ROWS_EVENT/UPDATE_ROWS_EVENT/DELETE_ROWS_EVENT、XID_EVENT事务提交。如果你面临的报错发生在一个事务的中间某个事件上sql_slave_skip_counter 1只会跳过一个事件SQL线程可能继续卡在同一个事务的下一个事件上。这时候需要多跳几次或者跳过一个完整事务的所有事件。所以更稳妥的操作是停掉SQL线程后查询当前错误对应的relay log位置把该事务包含的所有事件数量数清楚再一次性设置sql_slave_skip_counter为对应事件数。但这种方式容易数错实际运维中更常见的做法是1,1这样反复跳几次直到SHOW SLAVE STATUS\G不再报错为止。需要注意的是这种跳过本质是抛弃主库binlog中的某个事件如果该事件对应的数据变更没有在从库执行过那从库和主库之间就产生了永久性的数据差异。所以跳过之后一定要对涉及的表做数据一致性校验否则等于埋雷。3.2 方案二GTID模式下消费冲突事务MySQL 5.7.6之后GTID成为主流复制模式也全面转向GTID。GTID模式下没有sql_slave_skip_counter可用处理Duplicate entry的思考方式要从跳过某个事件变成消费掉某个GTID事务。首先在从库上执行SHOW SLAVE STATUS\G找到Retrieved_Gtid_Set和Executed_Gtid_Set。如果SQL线程是因为某个GTID事务执行失败而停止这个GTID会同时出现在Retrieved_Gtid_Set中但没有出现在Executed_Gtid_Set里它就是卡住的那个事务。处理方式是把这个GTID对应的空事务注入到从库的GTID集合中让SQL线程认为该事务已经执行过STOP SLAVE; -- 假设卡住的GTID是 4b0b3e91-8d2e-11ec-a48f-525400fa8b8e:123456 SET GTID_NEXT4b0b3e91-8d2e-11ec-a48f-525400fa8b8e:123456; BEGIN; COMMIT; SET GTID_NEXTAUTOMATIC; START SLAVE;这个BEGIN; COMMIT;的作用就是构造一个空事务然后把它的GTID标记为已执行。对MySQL来说这个GTID已经被消费掉了SQL线程重启后会从下一个GTID继续回放。此方法同样有数据一致性风险因为空事务意味着原本那个事务里的数据变更并没有真正落在从库上。如果原本的事务是INSERT从库会缺失这条数据如果原本的事务是UPDATE从库会停留在更新前的旧值。所以操作完后照样要对该表跑一致性校验。GTID模式下还有一种更规范的思路如果冲突的数据确实在从库上已经存在且与主库一致可以直接通过修改从库数据来补齐一致性而不是跳过事务。但这里有一个更优雅的做法——如果从库上那条冲突行和主库完全一致直接用REPLACE INTO或DELETE INSERT的方式把这条数据重置一遍让从库拥有这条记录然后手动把卡住的GTID事务注入为已完成。这样做的逻辑是既然数据已经一致事务内容已经失去意义空提交一个GTID等于告诉复制链路这单已经结清。3.3 方案三重搭从库最稳的保底手段如果数据不一致的范围很大或者你已经对当前从库的数据状态失去信心最稳妥的方案不是继续修修补补而是直接重搭从库。重搭的流程核心就三步备份主库、恢复到从库、重新配置复制。这里我不展开全部命令只把最容易踩坑的细节讲清楚。备份主库时如果是小数据量比如几十GB以内用mysqldump逻辑备份mysqldump -uusername -p --single-transaction --master-data2 --set-gtid-purgedON --all-databases backup.sql注意两个参数--single-transaction基于InnoDB的一致性快照备份过程中不锁表业务无感知。如果目标实例上有MyISAM表这个参数不生效需要配合--lock-tables或--flush-tables-with-read-lock。--master-data2在备份文件的头部以注释形式记录主库当前的binlog位点传统位点模式或GTID集合GTID模式。--set-gtid-purgedON会导出一份SET GLOBAL.GTID_PURGED语句恢复时会把主库已经执行过的GTID集合设置到从库上避免重复执行历史事务。如果数据量大到用mysqldump备份需要好几个小时就别折腾逻辑备份了直接用Percona XtraBackup做物理备份。物理备份是文件级别的拷贝速度远快于逻辑备份并且天然包含一致性快照恢复后不需要再重放binlog日志。恢复完成后在从库上配置复制GTID模式指定MASTER_AUTO_POSITION1MySQL会自动根据GTID_PURGED来同步从库缺失的事务集合不用手动指定文件和位点。传统位点模式从备份文件头部找到MASTER_LOG_FILE和MASTER_LOG_POS在CHANGE MASTER TO里明确指定。重搭从库虽然耗时但它是唯一能保证从库与主库完全一致的方案。在业务可接受的维护窗口内如果数据一致性已经无法通过局部修复来保证我强烈建议优先考虑重搭而不是在一条坏掉的数据上反复纠缠。4. 从根源上防住Duplicate entry4.1 从库只读参数与权限管控Duplicate entry这个错误绝大多数情况下是从库被写入了不该写的数据导致的。所以从根源上防住它第一步就是让从库不能写。MySQL提供了两个参数来控制从库的写入权限read_only1普通用户不能执行写操作但拥有SUPER权限的用户比如root仍然可以写。super_read_only1连SUPER权限的写操作也被禁止只有复制线程可以写入。建议在从库的配置文件中同时设置这两个参数[mysqld] read_only 1 super_read_only 1需要说明的是super_read_only不是MySQL官方所有版本都支持的参数5.7.8及以上版本才支持。但现在的生产环境基本都跑在5.7以上可以放心使用。设置完成后业务账号如果误连从库执行DML会直接报The MySQL server is running with the --read-only option从复制层面就挡住了人为写入的可能。这个操作对防止Duplicate entry是决定性的一步强烈建议所有主从架构都开启。不过要注意一个场景复制链路本身需要写入relay log和更新系统表有些高可用方案比如MHA、Orchestrator在执行主从切换时需要临时关闭只读。这时候管理人员要在切换脚本里动态处理read_only参数切换完成后再恢复。我见过有的自动切换脚本把从库拉起为新的主库后忘了去掉read_only结果新的主库也是只读的业务写入全部失败这种事故比Duplicate entry要严重得多。4.2 备份恢复与主从初始化时的坑重搭从库时最容易埋下Duplicate entry隐患的就是GTID集合处理不正确。这里重点讲两个坑。第一个坑从库GTID集合比主库多了一些事务。恢复备份时如果GTID_PURGED设置不正确从库可能认为自己已经执行过某些事务但这些事务实际上并没有落地到数据上。后面主库再推送这些事务时从库的SQL线程会直接忽略掉数据就永远差了一块。更严重的是如果从库GTID集合里包含一个主库根本没有的GTID复制链路可能在CHANGE MASTER TO时就报错或者在回放时跳过本该执行的事务。第二个坑应用二进制日志恢复mysqlbinlog时操作不当。如果之前做过基于binlog的增量恢复恢复时没有正确过滤掉已经在从库执行过的事务恢复后从库的同一条数据可能被插入两次直接引起Duplicate entry。使用mysqlbinlog恢复的时候务必配合--stop-datetime或--stop-position精确定位恢复边界避免重复应用。重搭从库时我还建议加一个保险恢复完备份之后先别急着START SLAVE先做一次数据校验。只对核心业务表做一次CHECKSUM TABLE比对主从两边确认恢复的数据没有缺漏再开启复制。这一步的成本很低但能避免很多隐性不一致。4.3 一致性巡检与日常监控即使从库开了read_only也挡不住主库binlog回放本身可能产生的不一致。比如半同步复制会在某些超时场景下自动降级为异步复制期间主库宕机切换后从库可能缺失一部分事务。这种场景下复制链路不会报错但数据已经悄悄对不上了等后续写入撞上重复主键Duplicate entry就爆发出来了。所以日常运维一定要配置一致性巡检。最常用的工具是Percona Toolkit里的pt-table-checksum和pt-table-sync。pt-table-checksum的原理是对主库每个表做一次CRC32校验把校验值和每一行数据通过binlog同步到从库再从从库上读取同样的校验值进行比对。它可以自动忽略复制延迟采用分批chunk的方式不会锁住整个表。pt-table-checksum --host主库地址 --userchecksum_user --passwordxxx --databases你的业务库名 --tables核心业务表 --recursion-methodprocesslist输出会有一列DIFFS数字为0表示一致非0表示不一致。发现不一致后再用pt-table-sync修复pt-table-sync --host主库地址 --userchecksum_user --passwordxxx --databases你的业务库名 --tables核心业务表 --replicatepercona.checksums --executept-table-sync会先算出主从差异再把从库修正为与主库一致。它支持只打印修复语句不执行--dry-run建议实际执行前先跑一遍dry-run把可能的修复语句人工审一遍再放行。这一步很重要历史上出现过pt-table-sync在特定主键分布下生成不合理的DELETE语句的情况直接删除大范围数据比Duplicate entry可怕多了。日常监控方面除了SHOW SLAVE STATUS\G的Slave_SQL_Running状态还建议用Prometheus mysqld_exporter采集mysql_slave_status_slave_sql_running指标对这个值配置2~3分钟的告警。很多团队只监控了Seconds_Behind_Master这个字段在SQL线程停止时有时候显示NULL等你发现的时候从库可能已经落后了十几分钟回放压力更大。直接监控Slave_SQL_Running和Last_SQL_Error_Timestamp才是精准的告警信号。5. 问题排查速查表与实录经验5.1 常见现象排查速查表报错/现象可能原因排查方向处理建议Duplicate entry xxx for key PRIMARY从库已存在该主键数据对比主从库同一条记录数据一致则跳过/注入GTID不一致则先修正或删除从库多余数据再继续复制Duplicate entry xxx for key uk_name从库已存在该唯一键数据对比唯一键对应记录同理但注意唯一键冲突可能由两条不同主键的数据引发需要精确定位Slave_SQL_RunningNo且Last_SQL_Errno1062SQL线程因主键冲突停止查SHOW SLAVE STATUS\G按本文方案二或三处理Slave_IO_RunningYes但Seconds_Behind_Master持续增长SQL线程阻塞或慢事务查SHOW PROCESSLIST确认是否有锁等待排查长事务、锁竞争必要时KILL阻塞会话GTID模式下sql_slave_skip_counter报错参数已废弃使用GTID注入方式参考3.2方案二跳过事务后从库数据与主库长期不一致跳过的数据变更未落地用pt-table-checksum巡检根据差异范围决定局部修复还是重搭从库这个表格里我想额外强调一行Duplicate entry的重复键不一定非是主键也可能是唯一索引键。报错里会明确写for key uk_xxx或for key PRIMARY处理逻辑一致但定位具体冲突行时查询条件要换成对应的唯一键字段而不是主键字段。5.2 实战中的几条关键经验最后分享几条我在实际运维中沉淀下来的经验可能比技巧本身更重要不要一上来就跳过错误。跳过是最快的恢复手段但也是最容易制造长期隐患的操作。每跳过一个事务从库就多一份与主库不一致的风险。正确姿势是先花两分钟确认冲突数据的状态再决定是跳、是修、还是重搭。这十几分钟的检查成本远比后续反复处理不一致要低。从库只读一定是常态不是例外。我接过好几个从库频繁报Duplicate entry的案例最后几乎都指向同一个原因开发环境或测试环境的某个服务直连了从库定时任务在从库上执行了写操作。开启read_only和super_read_only之后这类问题直接从源头消失。存储过程、定时事件也要检查。从库即使开了read_onlyEVENT调度器event_scheduler仍然可以执行写操作因为事件调度器运行时有SUPER权限。如果担心事件引起写入记得在从库上设置event_schedulerOFF。定期做一致性巡检比出事再修划算得多。pt-table-checksum跑一次全库比对数据量在百GB级别时大概几十分钟到几小时。相比半夜爬起来处理复制中断和事后漫长的数据核对这点成本简直不值一提。我现在管理的实例每周固定跑一次一致性巡检偏离检出率能降低九成以上。重搭从库很多时候是最优解。有些朋友对于修从库有执念遇到不一致就想着怎么把binlog补齐、怎么把差异数据修回来。如果从库数据错乱已经比较严重与其花几小时精修数据还不一定对不如直接重搭。mysqldump备份加恢复搭一个从库数据量在百GB级别通常一两个小时就能搞定比你纠结一天强多了。报警信息要带上下文。监控告警里不要只发Slave_SQL_RunningNo要把Last_SQL_Error、Last_SQL_Error_Timestamp、Retrieved_Gtid_Set、Executed_Gtid_Set一起带上。现场信息越完整处理速度越快这个细节在值班时尤其重要。处理MySQL主从同步的Duplicate entry问题本质上就一句话先定位数据从哪里来再决定是让事务通过还是让数据对齐。只要顺着这条思路走不管报错里的表是什么、键是什么都不会再被卡住。希望这篇实操笔记能帮你少踩几个坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价