MySQL 主从 AUTO_INCREMENT 不一致问题分析
作者:vivo 互联网数据库团队 - Wei Haodong
本文介绍了 MySQL5.7 中常见的replace into 操作造成的主从auto_increment不一致现象,一旦触发了主从切换,业务的正常插入操作会触发主键冲突的报错提示。
一、问题描述
1.1 问题现象
在 MySQL 5.7 版本中,REPLACE INTO 操作在表存在自增主键的情况下,可能会出现表的auto_increment值主从不一致现象,如果在此期间发生主从故障切换,当原来的slave节点变成了新的master节点,由于表的auto_increment值是小于原主库的,当业务继续写入时,就会收到主键冲突的报错提示。
相关报错信息如下:
! 报错提示
ERROR 1062 (23000): Duplicate entry 'XXX' for key 'PRIMARY'
1.2 影响评估
在业务逻辑中使用了Replace into,或者INSERT...ON DUPLICATE KEY UPDATE。
一旦出现了表的auto_increment值主从不一致现象,在出现MySQL主从故障切换后,业务的正常写入会报主键冲突的错误,当auto_increment相差不多,或许在业务重试的时候会跳过报错,但是auto_increment相差较多时,会超出业务重试的次数,这样造成的影响会更大。
二、问题复现
2.1 环境搭建
这里在测试环境中,搭建MySQL社区版 5.7 版本,一主一从的架构。
【OS】:CentOS Linux release 7.3
【MySQL】:社区版本 5.7
【主从架构】:一主一从
【库表信息】:库名:test2023
表名:test_autoincrement
表结构如下:
2.2 准备测试数据
正常情况下,插入一行数据,影响的行数是1。
此时查看主从节点表的autoincrement值,可以看到此时主从的AUTO_INCREMENT是一致的,都是4,即自增主键下一次申请的值是4。
2.3 问题复现模拟
2.3.1 模拟REPLACE INTO操作
MySQL [test2023]> REPLACE INTO test_autoincrement (name,uid) values('张三丰',1001);
Query OK, 2 rows affected (0.01 sec)
这里通过REPLACE INTO操作判断,如果存在唯一ID为1001的记录,那么将name字段的值更改为"张三丰",可发现此时影响的行数是2。现在我们再次查看主从节点表的autoincrement值。
此时出现了主从节点表的AUTO_INCREMENT不一致现象。
2.3.2 模拟主从切换
由于是在测试环境,这里就直接进行了主从关系的更改。
(1)停止当前slave节点的复制线程
MySQL [test2023]> stop slave;
Query OK, 0 rows affected (0.08 sec)
(2)查看当前slave节点的Executed_Gtid_Set值
(3)重做主从关系
2.3.3 模拟业务正常写入
MySQL [test2023]> insert into test_autoincrement(name,uid) select '赵六',1004;
ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'
到这里我们看到了预期的报错现象,如果是正常业务系统,这里的主从节点表的AUTO_INCREMENT可能会相差非常大,业务的正常插入就会持续报错了。
意味着真实的操作是先做delete操作,然后再进行insert。
三、原因分析
3.1 为什么从库节点的 autoincrement 没有变化?
这里可以看到REPLACE INTO操作对应的binlog日志记录其实是update操作,从库节点在应用update操作时,发现命中数据时,对应的autoincrement是没有变化的。
3.2 REPLACE INTO 操作的官方定义是什么?
官方对于 REPLACE INTO 的定义如下:
摘选自
REPLACE works exactly like INSERT, except that if an old row in the table has the same value as a new row for a PRIMARY KEY or a UNIQUE index, the old row is deleted before the new row is inserted. See Section 13.2.5, “INSERT Statement”.
REPLACE is a MySQL extension to the SQL standard. It either inserts, or deletes and inserts. For another MySQL extension to standard SQL—that either inserts or updates—see Section 13.2.5.2, “INSERT ... ON DUPLICATE KEY UPDATE Statement”.
这里可以看到一张表包含主键或者唯一键的情况下,replace操作会判断原有的数据行是否存在,如果存在的话,就先删除旧的数据,然后进行insert操作,如果不存在的话,就和insert操作时一样的。
第二段也提到了INSERT ... ON DUPLICATE KEY UPDATE Statement ,其实这个操作也会造成上面的主从autoincrement不一致现象,这里就不展开讨论了。
! Note
REPLACE makes sense only if a table has a PRIMARY KEY or UNIQUE index. Otherwise, it becomes equivalent to INSERT, because there is no index to be used to determine whether a new row duplicates another.
3.3 为什么REPLACE INTO操作在binlog日志中记录的是update操作?
这里我们通过源码文件sql_insert.cc和log_event.cc进行分析。
上述源码中可以看到在主库中replace 操作其实是insert 或者 delete + insert
The manual defines the REPLACE semantics that it is either an INSERT or DELETE(s) + INSERT;
而 MySQL 在主从同步的binlog日志中,将replace操作转换为update操作的条件为:当发生冲突的键是最后一个唯一键,且没有外键约束,且没有触发器,由于我们的测试表中是没有外键约束,也没有触发器的,所以从库接收到的binlog日志中转化为update的条件即为最后一个唯一键。
这里,我们再进行测试一下(去掉表中的唯一索引uid)。
(1)创建新表
CREATE TABLE `test_autoincrement_2` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(100) NOT NULL DEFAULT 'test' COMMENT '测试名字',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4
(2)插入测试数据
(3)replace into 操作验证主库和从库的AUTO_INCREMENT
MySQL [test2023]> REPLACE INTO test_autoincrement_2 (id,name) values(3,'郑十');
Query OK, 2 rows affected (0.08 sec)
这里我们把id=3的这一行数据对应的name修改为’郑十’,可发现上述影响的行数是2。
再次验证主库和从库的AUTO_INCREMENT,发现并没有发生变化,还是4。
CREATE TABLE `test_autoincrement_2` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
`name` varchar(100) NOT NULL DEFAULT 'test' COMMENT '测试名字',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4
(4)分析binlog日志文件
总结:可发现binlog日志记录的同样是update 操作。只是当表中除了主键外没有额外的唯一键时,replace into的操作并不会触发从库的auto_increment的异常问题。比如上述的案例REPLACE INTO test_autoincrement_2 (id,name) values(3,'郑十');,这里仅更改了name字段,由‘吴九‘修改为’郑十’。但是主键id是没有变化的,当然也就不需要再次使用auto_increment,这里也可以看到主库的auto_increment当然也没有发现变化(当表中除了主键外含有额外的唯一键时,是会触发申请auto_increment的),binlog接收的仍然是update操作,所以从库的auto_increment也是没有变化的,这样就没法造成auto_increment和主库不一致的问题了。
四、解决方案
到这里,我们是明白了replace into 会造成主从的auto_increment 不一致,但是怎么去解决呢?
4.1 升级到 MySQL 8.0 版本
在 MySQL 8.0 版本中已将AUTO_INCREMENT值做了持久化,且在做更新操作时,会将表上的自增列被更新为比auto_increment更大的值,auto_increment值也将被更新。
4.2 修改 AUTO_INCREMENT 值
线上环境可能已经有很多这种情况,在没有触发业务报错的情况下,一般是很难发现这个隐患,如何在日常巡检中找到这些问题才是关键。
巡检逻辑一:这里可以通过巡检判断从库的max(id) >= AUTO_INCREMENT的方式来找出已经存在问题的表信息。然后通过SQL语句:ALTER TABLE table_name AUTO_INCREMENT = new_value; 进行修改。
巡检步骤可参考:
(1)仅检测某从节点,包含auto_increment 属性的表,过滤SQL如下:
select TABLE_SCHEMA,TABLE_NAME,AUTO_INCREMENT from information_schema.tables where table_schema not in ('information_schema','mysql','performance_schema','sys') AUTO_INCREMENTis not null \G
(2)加锁后读表信息,语句如下:
① 给表加锁
lock tables table_name write;
②读取数据和表auto_increment值进行比对
MAXID=select max(id) from table_name;
AUTO_INCREMENT=select AUTO_INCREMENT from information_schema.tables where TABLE_NAME='t1' ;
③ 判断条件
如果MAXID >= AUTO_INCREMENT , 判断为异常
巡检逻辑二:可以在高可用切换的时候增加AUTO_INCREMENT值判断,如果AUTO_INCREMENT值不一致,则不发生切换,不过这里的slave节点AUTO_INCREMENT的值本身可能因为延迟等问题,就会稍落后maste主节点,正常的巡检还是有难度的,还有就是当MySQL主从切换触发时,如果是因为原主库宕机了,不触发切换也会有问题,所以还是需要提前尽快把这个隐患排除掉。
4.3 禁用 replace into 操作
业务侧禁用replace into 或 insert ... on duplicate key update ,实现方式可以通过代码逻辑来实现。
4.4 replace into操作的表不增加其他唯一索引
这里其实实现还是有难度的,自增id是不可控的,业务一般是不会使用数据库自带的自增id。
五、问题总结
1. REPLACE INTO 操作在表存在自增主键且包含唯一索引的情况下,当出现数据冲突的时候,会触发AUTO_INCREMENT在主从节点的不一致,一旦主从发生切换,就会造成业务的写入报主键冲突的错误。解决建议:业务更改实现方式,避免使用replace into,或者使用MySQL8.0 及以上的版本来解决该问题。
2. 该问题是一个官方的BUG,不过并没有在MySQL5.7的版本中得到修复 。
参考文献:
END
猜你喜欢
本文分享自微信公众号 - vivo互联网技术(vivoVMIC)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
JavaScript 引擎 V8 的内存沙盒 (V8 Sandbox) 开始测试
JavaScript 引擎 V8 宣布其内存沙盒 (V8 Sandbox)自最初的设计文档发布以来,已历经近三年时间,其间发布了数百个 CL,现在它已不再被视为实验性安全功能,并将在 Chrome 123 中进入测试阶段。 公告写道,过去三年(2021 年至 2023 年)发生的所有 Chrome 漏洞都是从 Chrome 渲染器进程中的内存损坏漏洞开始,该漏洞被用于远程代码执行 (RCE)。其中,60% 是 V8 中的漏洞。 然而,有一个问题:V8 漏洞很少是经典的内存损坏错误(如释放后使用、越界访问等),而是微妙的逻辑问题,这些问题反过来可以被利用来损坏内存。 因此,现有的内存安全解决方案大部分不适用于 V8。特别是,无论是切换到内存安全语言(例如 Rust),还是使用当前或未来的硬件内存安全功能(例如内存标记),都无法帮助解决 V8 当前面临的安全挑战。 不过在 V8 中发现和利用的几乎所有漏洞都有一个共同点:最终的内存损坏必然发生在 V8 堆内存,因为编译器和运行时(几乎)专门在 V8HeapObject实例上运行。 基于此,V8 团队设计了沙盒机制。V8 沙盒的设计思想是隔离...
- 下一篇
MySQL 核心模块揭秘 | 12 期 | 创建 savepoint
回滚操作,除了回滚整个事务,还可以部分回滚。部分回滚,需要保存点(savepoint)的协助。本文我们先看看保存点里面都有什么。 作者:操盛春,爱可生技术专家,公众号『一树一溪』作者,专注于研究 MySQL 和 OceanBase 源码。 爱可生开源社区出品,原创内容未经授权不得随意使用,转载请联系小编并注明来源 本文基于 MySQL 8.0.32 源码,存储引擎为 InnoDB。 1. undo 日志序号 InnoDB 的事务对象有一个名为 undo_no 的属性。事务每次改变(插入、更新、删除)某个表的一条记录,都会产生一条 undo 日志。这条 undo 日志中会存储它自己的序号。这个序号就来源于事务对象的 undo_no 属性。 也就是说,事务对象的 undo_no 属性中保存着事务改变(插入、更新、删除)某个表中下一条记录产生的 undo 日志的序号。 每个事务都维护着各自独立的 undo 日志序号,和其它事务无关。 每个事务的 undo 日志序号都从 0 开始。事务产生的第 1 条 undo 日志的序号为 0,第 2 条 undo 日志的序号为 1,依此类推。 InnoDB...
相关文章
文章评论
共有0条评论来说两句吧...