news 2026/9/13 6:21:47

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL主从复制Duplicate entry错误原因与处理方案

这个报错,只要是亲手维护过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 event:SQL线程正在执行一个"写入行"的事件。Write_rows_event是binlog中记录行变更的事件类型之一,对应的是INSERT操作。行级复制(binlog_format=ROW)下,主库的每一次插入都会在binlog中变成这样一个事件,从库拿到后重放到本地表。
  • on table xxx.xxx:哪个库哪张表出错了,这里会明确给出库名和表名,比如testdb.user
  • Duplicate entry 'xxx' for key 'PRIMARY':这是真正的失败原因,翻译成人话就是——从库上这张表里已经有主键值为xxx的行了,你再插一条同样的,主键唯一性约束不允许。

所以整条报错的完整含义是:从库在回放主库binlog里的一条INSERT操作时,发现目标表中已经存在相同主键的数据,插入失败,SQL线程停止,主从同步中断。这个错之所以经典,是因为它几乎是"从库数据不一致"的最典型信号。

1.2 为什么从库会写入失败

主库和从库各自维护一份数据,正常情况下主库写入什么,从库就照着写什么。从库报Duplicate entry,只说明一个事实:从库上已经有这条数据了,但主库的binlog里又出现了一次相同主键的写入。

最常见的三种成因:

  1. 从库被手动写入过数据。开发同学连错库,在从库上执行了一条INSERT或UPDATE,恰好这条数据的主键又落在主库后续binlog的写入范围内。于是主库正常插入,从库重放时主键撞车。
  2. 复制中断后手动跳过事务导致的数据错位。之前主从同步就报过错,DBA为了临时顶上业务,执行了set global sql_slave_skip_counter=1跳过了一个或多个事务。跳过的这些事务里可能包含INSERT,也可能包含DELETE和UPDATE,跳完之后数据已经和主库对不上了,后续新进来的写入事件就可能在从库撞车。
  3. 备份恢复或重建从库时数据没对齐。比如用某个旧备份搭从库,备份里的数据本身就和主库不一致;或者恢复备份后binlog位点(GTID集合)指错了位置,从库把主库之前已经执行过的一部分事务又重放了一遍。

这三种成因对应的处理思路完全不同,所以别急着动手修,第一步是先搞清楚为什么会重复。

2. 修复前的三步检查:先别急着改数据

2.1 确认主从状态与错误位置

看到报错后,第一件事不是直接跳过错误,而是完整确认当前复制链路的状态。在从库上执行:

SHOW SLAVE STATUS\G

重点关注几个字段:

  • Slave_IO_Running:IO线程是否正常,正常情况下是Yes。如果IO线程也断了,说明网络或主库binlog推送有问题,那是另一条排查线。
  • Slave_SQL_Running:SQL线程是否正常,报错场景下通常是No
  • Last_SQL_ErrorLast_SQL_Error_Timestamp:记录SQL线程最后一次报错的详细信息,就是我们看到的Duplicate entry错误。
  • Exec_Master_Log_PosRead_Master_Log_Pos:SQL线程执行到的位点,以及IO线程拉取到的位点。两者差值越大,说明从库落后越多。
  • Retrieved_Gtid_SetExecuted_Gtid_Set(GTID模式下):记录已经拉取和已经执行的事务集合,对比能看出具体卡在哪个事务上。
  • 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_counter=1跳过错误的操作,结果跳出一个数据长期不一致的坑,后续每几天就报一次错,天天半夜被叫起来处理。

2.3 备份现场,给自己留后路

在动手修改之前,无论采用哪种修复方案,都建议在从库上做一次备份,即使只是单表也要做。别嫌麻烦,有一次我跳过一个事务后才发现那个事务里还包含一个隐性的DDL变更,导致从库上一个字段的定义和主库不一致,最后只能重新搭库。如果有备份,回滚成本会低很多。

对于单表数据,最轻量的方式是:

mysqldump -uusername -p --set-gtid-purged=OFF --single-transaction --skip-lock-tables xxx xxx > /tmp/backup_xxx_$(date +%F).sql

注意--set-gtid-purged=OFF这个参数,备份从库数据时如果不加,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_EVENTQUERY_EVENT(事务开始)、多个WRITE_ROWS_EVENT/UPDATE_ROWS_EVENT/DELETE_ROWS_EVENTXID_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_SetExecuted_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_NEXT='4b0b3e91-8d2e-11ec-a48f-525400fa8b8e:123456'; BEGIN; COMMIT; SET GTID_NEXT='AUTOMATIC'; START SLAVE;

这个BEGIN; COMMIT;的作用就是构造一个空事务,然后把它的GTID标记为已执行。对MySQL来说,这个GTID已经被消费掉了,SQL线程重启后会从下一个GTID继续回放。

此方法同样有数据一致性风险,因为空事务意味着原本那个事务里的数据变更并没有真正落在从库上。如果原本的事务是INSERT,从库会缺失这条数据;如果原本的事务是UPDATE,从库会停留在更新前的旧值。所以操作完后,照样要对该表跑一致性校验。

GTID模式下还有一种更规范的思路:如果冲突的数据确实在从库上已经存在且与主库一致,可以直接通过修改从库数据来"补齐一致性",而不是跳过事务。但这里有一个更优雅的做法——如果从库上那条冲突行和主库完全一致,直接用REPLACE INTODELETE + INSERT的方式把这条数据重置一遍,让从库拥有这条记录,然后手动把卡住的GTID事务注入为已完成。这样做的逻辑是:既然数据已经一致,事务内容已经失去意义,空提交一个GTID等于告诉复制链路"这单已经结清"。

3.3 方案三:重搭从库,最稳的保底手段

如果数据不一致的范围很大,或者你已经对当前从库的数据状态失去信心,最稳妥的方案不是继续修修补补,而是直接重搭从库。重搭的流程核心就三步:备份主库、恢复到从库、重新配置复制。这里我不展开全部命令,只把最容易踩坑的细节讲清楚。

备份主库时,如果是小数据量(比如几十GB以内),用mysqldump逻辑备份:

mysqldump -uusername -p --single-transaction --master-data=2 --set-gtid-purged=ON --all-databases > backup.sql

注意两个参数:

  • --single-transaction:基于InnoDB的一致性快照,备份过程中不锁表,业务无感知。如果目标实例上有MyISAM表,这个参数不生效,需要配合--lock-tables--flush-tables-with-read-lock
  • --master-data=2:在备份文件的头部以注释形式记录主库当前的binlog位点(传统位点模式)或GTID集合(GTID模式)。--set-gtid-purged=ON会导出一份SET @@GLOBAL.GTID_PURGED语句,恢复时会把主库已经执行过的GTID集合设置到从库上,避免重复执行历史事务。

如果数据量大到用mysqldump备份需要好几个小时,就别折腾逻辑备份了,直接用Percona XtraBackup做物理备份。物理备份是文件级别的拷贝,速度远快于逻辑备份,并且天然包含一致性快照,恢复后不需要再重放binlog日志。

恢复完成后,在从库上配置复制:

  • GTID模式:指定MASTER_AUTO_POSITION=1,MySQL会自动根据GTID_PURGED来同步从库缺失的事务集合,不用手动指定文件和位点。
  • 传统位点模式:从备份文件头部找到MASTER_LOG_FILEMASTER_LOG_POS,在CHANGE MASTER TO里明确指定。

重搭从库虽然耗时,但它是唯一能保证从库与主库完全一致的方案。在业务可接受的维护窗口内,如果数据一致性已经无法通过局部修复来保证,我强烈建议优先考虑重搭,而不是在一条坏掉的数据上反复纠缠。

4. 从根源上防住Duplicate entry

4.1 从库只读参数与权限管控

Duplicate entry这个错误,绝大多数情况下是从库被写入了不该写的数据导致的。所以从根源上防住它,第一步就是让从库"不能写"。

MySQL提供了两个参数来控制从库的写入权限:

  • read_only=1:普通用户不能执行写操作,但拥有SUPER权限的用户(比如root)仍然可以写。
  • super_read_only=1:连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-checksumpt-table-sync

pt-table-checksum的原理是对主库每个表做一次CRC32校验,把校验值和每一行数据通过binlog同步到从库,再从从库上读取同样的校验值进行比对。它可以自动忽略复制延迟,采用分批chunk的方式,不会锁住整个表。

pt-table-checksum --host=主库地址 --user=checksum_user --password=xxx --databases=你的业务库名 --tables=核心业务表 --recursion-method=processlist

输出会有一列DIFFS,数字为0表示一致,非0表示不一致。发现不一致后,再用pt-table-sync修复:

pt-table-sync --host=主库地址 --user=checksum_user --password=xxx --databases=你的业务库名 --tables=核心业务表 --replicate=percona.checksums --execute

pt-table-sync会先算出主从差异,再把从库修正为与主库一致。它支持只打印修复语句不执行(--dry-run),建议实际执行前先跑一遍dry-run,把可能的修复语句人工审一遍再放行。这一步很重要,历史上出现过pt-table-sync在特定主键分布下生成不合理的DELETE语句的情况,直接删除大范围数据,比Duplicate entry可怕多了。

日常监控方面,除了SHOW SLAVE STATUS\GSlave_SQL_Running状态,还建议用Prometheus + mysqld_exporter采集mysql_slave_status_slave_sql_running指标,对这个值配置2~3分钟的告警。很多团队只监控了Seconds_Behind_Master,这个字段在SQL线程停止时有时候显示NULL,等你发现的时候,从库可能已经落后了十几分钟,回放压力更大。直接监控Slave_SQL_RunningLast_SQL_Error_Timestamp才是精准的告警信号。

5. 问题排查速查表与实录经验

5.1 常见现象排查速查表

报错/现象可能原因排查方向处理建议
Duplicate entry 'xxx' for key 'PRIMARY'从库已存在该主键数据对比主从库同一条记录数据一致则跳过/注入GTID;不一致则先修正或删除从库多余数据再继续复制
Duplicate entry 'xxx' for key 'uk_name'从库已存在该唯一键数据对比唯一键对应记录同理,但注意唯一键冲突可能由两条不同主键的数据引发,需要精确定位
Slave_SQL_Running=NoLast_SQL_Errno=1062SQL线程因主键冲突停止SHOW SLAVE STATUS\G按本文方案二或三处理
Slave_IO_Running=YesSeconds_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 实战中的几条关键经验

最后分享几条我在实际运维中沉淀下来的经验,可能比技巧本身更重要:

  1. 不要一上来就跳过错误。跳过是最快的恢复手段,但也是最容易制造长期隐患的操作。每跳过一个事务,从库就多一份与主库不一致的风险。正确姿势是:先花两分钟确认冲突数据的状态,再决定是跳、是修、还是重搭。这十几分钟的检查成本,远比后续反复处理不一致要低。

  2. 从库只读一定是常态,不是例外。我接过好几个从库频繁报Duplicate entry的案例,最后几乎都指向同一个原因:开发环境或测试环境的某个服务直连了从库,定时任务在从库上执行了写操作。开启read_onlysuper_read_only之后,这类问题直接从源头消失。

  3. 存储过程、定时事件也要检查。从库即使开了read_onlyEVENT调度器(event_scheduler)仍然可以执行写操作,因为事件调度器运行时有SUPER权限。如果担心事件引起写入,记得在从库上设置event_scheduler=OFF

  4. 定期做一致性巡检比出事再修划算得多pt-table-checksum跑一次全库比对,数据量在百GB级别时大概几十分钟到几小时。相比半夜爬起来处理复制中断和事后漫长的数据核对,这点成本简直不值一提。我现在管理的实例,每周固定跑一次一致性巡检,偏离检出率能降低九成以上。

  5. 重搭从库很多时候是最优解。有些朋友对于"修从库"有执念,遇到不一致就想着怎么把binlog补齐、怎么把差异数据修回来。如果从库数据错乱已经比较严重,与其花几小时精修数据还不一定对,不如直接重搭。mysqldump备份加恢复搭一个从库,数据量在百GB级别通常一两个小时就能搞定,比你纠结一天强多了。

  6. 报警信息要带上下文。监控告警里不要只发Slave_SQL_Running=No,要把Last_SQL_ErrorLast_SQL_Error_TimestampRetrieved_Gtid_SetExecuted_Gtid_Set一起带上。现场信息越完整,处理速度越快,这个细节在值班时尤其重要。

处理MySQL主从同步的Duplicate entry问题,本质上就一句话:先定位数据从哪里来,再决定是让事务通过还是让数据对齐。只要顺着这条思路走,不管报错里的表是什么、键是什么,都不会再被卡住。希望这篇实操笔记能帮你少踩几个坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 6:19:32

Spring Boot图书借阅管理系统源码拆解:从数据库设计到并发控制

简介:一份基于Spring Boot的图书借阅管理系统高分毕设源码,附带数据库脚本与完整工程结构,面向高校计算机专业正在准备毕业设计、课程设计或期末大作业的学生,也适合需要Java Web项目实战练习的初级开发者。整个压缩包共122个文件…

作者头像 李华
网站建设 2026/9/13 6:19:00

I2C接口16位ADC ADS112C04驱动开发与调试指南

简介:一套完整的芯片驱动工程,基于官方固件库,可直接在主流集成开发环境中打开使用。它所驱动的模拟数字转换器具有十六位分辨率、四个输入通道,内置可编程增益放大器,适用于工业测量与传感器信号采集场景,…

作者头像 李华
网站建设 2026/9/13 6:16:54

BMI健康计算器Android源码落地:从zip解压到release打包全指南

简介:BMI健康计算器是一款用于评估人体体重与身高比例的安卓应用源码,压缩包内提供完整的Android Studio工程,适合初学安卓开发的读者通过实战项目理解健康类应用的开发要点。资源共26个文件,包括Java核心逻辑、XML界面布局、PNG图…

作者头像 李华
网站建设 2026/9/13 6:14:55

MySQL慢查询排查与优化:执行计划、索引设计与SQL改写实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 6:14:31

OpenClaw多智能体系统开发指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华