mysql 深度解析auto-increment自增列"Duliplicate key"问题
转载自:https://cloud.tencent.com/developer/article/1367681
问题描述
近期,线上有个重要Mysql客户的表在从5.6升级到5.7后master上插入过程中出现"Duplicate key"的错误,而且是在主备及RO实例上都出现。以其中一个表为例,迁移前通过“show create table” 命令查看的auto increment id为1758609, 迁移后变成了1758598,实际对迁移生成的新表的自增列用max求最大值为1758609。用户采用的是Innodb引擎,而且据运维同学介绍,之前碰到过类似问题,重启即可恢复正常。
内核问题排查
由于用户反馈在5.6上访问正常,切换到5.7后就报错。因此,首先得怀疑是5.7内核出了问题,因此第一反应是从官方bug list中搜索一下是否有类似问题存在,避免重复造车。经过搜索,发现官方有1个类似的bug,这里简单介绍一下该bug。
背景知识1:Innodb引擎中的auto increment 相关参数及数据结构
主要参数包括:innodb_autoinc_lock_mode用于控制获取自增值的加锁方式,auto_increment_increment, auto_increment_offset用于控制自增列的递增的间隔和起始偏移。
主要涉及的结构体包括:数据字典结构体,保存整个表的当前auto increment值以及保护锁;事务结构体,保存事务内部处理的行数;handler结构体,保存事务内部多行的循环迭代信息。
这部分网上有篇文章介绍的比较好,具体参见:(https://www.cnblogs.com/zengkefu/p/5683258.html)。
背景知识2:mysql及Innodb引擎中对autoincrement访问及修改的流程
(1) 数据字典结构体(dict_table_t)换入换出时对autoincrement值的保存和恢复。换出时将autoincrement保存在全局的的映射表中,然后淘汰内存中的dict_table_t。换入时通过查找全局映射表恢复到dict_table_t结构体中。相关的函数为dict_table_add_to_cache及dict_table_remove_from_cache_low。
(2) row_import, table truncate过程更新autoincrement。
(3) handler首次open的时候,会查询当前表中最大自增列的值,并用最大列的值加1来初始化表的data_dict_t结构体中的autoinc的值。
(4) insert流程。相关对autoinc修改的堆栈如下:
ha_innobase::write_row:write_row的第三步中调用handler句柄中的update_auto_increment函数更新auto increment的值
handler::update_auto_increment: 调用Innodb接口获取一个自增值,并根据当前的auto_increment相关变量的值调整获取的自增值;同时设置当前handler要处理的下一个自增列的值。
ha_innobase::get_auto_increment:获取dict_tabel中的当前auto increment值,并根据全局参数更新下一个auto increment的值到数据字典中
ha_innobase::dict_table_autoinc_initialize:更新auto increment的值,如果指定的值比当前的值大,则更新。
handler::set_next_insert_id:设置当前事务中下一个要处理的行的自增列的值。
(5) update_row。对于”INSERT INTO t (c1,c2) VALUES(x,y) ON DUPLICATE KEY UPDATE”语句,无论唯一索引列所指向的行是否存在,都需要推进auto increment的值。相关代码如下:
if (error == DB_SUCCESS
&& table->next_number_field
&& new_row == table->record[0]
&& thd_sql_command(m_user_thd) == SQLCOM_INSERT
&& trx->duplicates) {
ulonglong auto_inc;
……
auto_inc = table->next_number_field->val_int();
auto_inc = innobase_next_autoinc(auto_inc, 1, increment, offset, col_max_value);
error = innobase_set_max_autoinc(auto_inc);
……
}
从我们的实际业务流程来看,我们的错误只可能涉及insert及update流程。
BUG 76872 / 88321: "InnoDB AUTO_INCREMENT produces same value twice"
(1) bug概述:当autoinc_lock_mode大于0,且auto_increment_increment大于1时,系统刚重启后多线程同时对表进行insert操作会产生“duplicate key”的错误。
(2) 原因分析:重启后innodb会把autoincrement的值设置为max(id) + 1。此时,首次插入时,write_row流程会调用handler::update_auto_increment来设置autoinc相关的信息。首先通过ha_innobase::get_auto_increment获取当前的autoincrement的值(即max(id) + 1),并根据autoincrement相关参数修改下一个autoincrement的值为next_id。当auto_increment_increment大于1时,max(id) + 1 会不大于next_id。handler::update_auto_increment获取到引擎层返回的值后为了防止有可能某些引擎计算自增值时没有考虑到当前auto increment参数,会重新根据参数计算一遍当前行的自增值,由于Innodb内部是考虑了全局参数的,因此handle层对Innodb返回的自增id算出的自增值也为next_id,即将会插入一条自增id为next_id的行。handler层会在write_row结束的时候根据当前行的值next_id设置下一个autoincrement值。如果在write_row尚未设置表的下一个autoincrement期间,有另外一个线程也在进行插入流程,那么它获取到的自增值将也是next_id。这样就产生了重复。
(3) 解决办法:引擎内部获取自增列时考虑全局autoincrement参数,这样重启后第一个插入线程获取的自增值就不是max(id) + 1,而是next_id,然后根据next_id设置下一个autoincrement的值。由于这个过程是加锁保护的,其他线程再获取autoincrement的时候就不会获取到重复的值。
通过上述分析,这个bug仅在autoinc_lock_mode > 0 并且auto_increment_increment > 1的情况下会发生。实际线上业务对这两个参数都设置为1,因此,可以排除这个bug造成线上问题的可能性。
现场分析及复现验证
既然官方bug未能解决我们的问题,那就得自食其力,从错误现象开始分析了。
(1) 分析max id及autoincrement的规律 由于用户的表设置了ON UPDATE CURRENT_TIMESTAMP列,因此可以把所有的出错的表的max id、autoincrement及最近更新的几条记录抓取出来,看看是否有什么规律。抓取的信息如下:

乍看起来,这个错误还是很有规律的,update time这一列是最后插入或者修改的时间,结合auto increment及max id的值,现象很像是最后一批事务只更新了行的自增id,没有更新auto increment的值。联想到【官方文档】中对auto increment用法的介绍,update操作是可以只更新自增id但不触发auto increment推进的。按照这个思路,我尝试复现了用户的现场。复现方法如下:

同时在binlog中,我们也看到有update自增列的操作。如图:

不过,由于binlog是ROW格式,我们也无法判断这是内核出问题导致了自增列的变化还是用户自己更新所致。因此我们联系了客户进行确认,结果用户很确定没有进行更新自增列的操作。那么这些自增列到底是怎么来的呢?
(2) 分析用户的表及sql语句 继续分析,发现用户总共有三种类型的表(hz_notice_stat_sharding, hz_notice_group_stat_sharding,hz_freeze_balance_sharding),这三种表都有自增主键。但是前面两种都出现了autoinc错误,唯独hz_freeze_balance_sharding表没有出错。难道是用户对这两种表的访问方式不一样?抓取用户的sql语句,果然,前两种表用的都是replace into操作,最后一种表用的是update操作。难道是replace into语句导致的问题? 搜索官方bug, 又发现了一个疑似bug。
bug #87861: “Replace into causes master/slave have different auto_increment offset values”
原因: (1) Mysql对于replace into实际是通过delete + insert语句实现,但是在ROW binlog格式下,会向binlog记录update类型日志。Insert语句会同步更新autoincrement,update则不会。
(2) replace into在Master上按照delete+insert方式操作, autoincrement就是正常的。基于ROW格式复制到slave后,slave机上按照update操作回放,只更新行中自增键的值,不会更新autoincrement。因此在slave机上就会出现max(id)大于autoincrement的情况。此时在ROW模式下对于insert操作binlog记录了所有的列的值,在slave上回放时并不会重新分配自增id,因此不会报错。但是如果slave切master,遇到Insert操作就会出现”Duplicate key”的错误。
(3) 由于用户是从5.6迁移到5.7,然后直接在5.7上进行插入操作,相当于是slave切主,因此会报错。
解决方案
业务侧的可能解决方案: (1) binlog改为mixed或者statement格式 (2) 用Insert on duplicate key update代替replace into
内核侧可能解决方案: (1) 在ROW格式下如果遇到replace into语句,则记录statement格式的logevent,将原始语句记录到binlog。 (2) 在ROW格式下将replace into语句的logevent记录为一个delete event和一个insert event。
心得
(1) autoincrement的autoinc_lock_mode及auto_increment_increment这两个参数变化容易导致出现重复的key,使用过程中要尽量避免动态的去修改。
(2) 在碰到线上的问题时,首先应该做好现场分析,明确故障发生的场景、用户的SQL语句、故障发生的范围等信息,同时要对涉及实例的配置信息、binlog甚至实例数据等做好备份以防过期丢失。只有这样才能在找官方bug时精准的匹配场景,如果官方没有相关bug,也能通过已有线索独立分析。
mysql 深度解析auto-increment自增列"Duliplicate key"问题的更多相关文章
- 百万级数据下的mysql深度解析
首先,数据量大的时候,应尽量避免全表扫描,应考虑在 where 及 order by 涉及的列上建立索引,建索引可以大大加快数据的检索速度.但是,有些情况索引是不会起效的: 1.应尽量避免在 wher ...
- mongodb php auto increment 自增
mongodb的自增实现根oracle,postgresql是差不多,都是通过计数器来实现的. oracle自增实现: 实例说明oracle序列用法 postgresql自增实现: postgresq ...
- MySQL--自增列学习
##=====================================================================================## 在数据库表设计中会纠 ...
- Oracle 12c的自增列Identity Columns
在Oracle的12c版本中,Oracle实现了类似MySQL中的auto_increment的自增列,下面我们看一起Oracle是怎么实现的. Oracle Database 12c Enterpr ...
- MySQL--自增列持久化问题
====================================================================== 自增列持久化问题 5.5/5.6/5.7三个版本中,MyS ...
- Entity Framework DBContext 增删改查深度解析
Entity Framework DBContext 增删改查深度解析 有一段时间没有更新博客了,赶上今天外面下雨,而且没人约球,打算把最近对Entity Framework DBContext使用的 ...
- MySQL存储引擎之Spider内核深度解析
作者介绍 朱阅岸,中国人民大学博士,现供职于腾讯云数据库团队.研究方向主要为数据库系统理论与实现.新硬件平台下的数据库系统以及TP+AP型混合系统. Spider是为MySQL/MariaDB开发 ...
- 程序员收藏必看系列:深度解析MySQL优化(二)
程序员收藏必看系列:深度解析MySQL优化(一) 性能优化建议 下面会从3个不同方面给出一些优化建议.但请等等,还有一句忠告要先送给你:不要听信你看到的关于优化的“绝对真理”,包括本文所讨论的内容,而 ...
- mysql AUTO INCREMENT字段 语法
mysql AUTO INCREMENT字段 语法 作用:在新记录插入表中时生成一个唯一的数字 说明:我们通常希望在每次插入新记录时,自动地创建主键字段的值.我们可以在表中创建一个 auto-incr ...
随机推荐
- python基础之列表、元组
一.列表 列表(list)是我们最以后最常用的数据类型之一,通过列表可以对数据实现最方便的存储.修改等操作 作用:多个装备,多个爱好,多门课程,多个女朋友等 定义:[ ]内可以有多个任意类型的值,逗号 ...
- stm32.cube(一)——系统架构及目录结构
一.前言 Arm的应用场景往往比51单片机复杂得多,如果一个高级应用的开发需要连底层的结构性代码都要重构,那么在成本和研发周期上就会面临巨大的风险.为了简化编码过程,芯片厂商经常会提供一些板卡级支持的 ...
- 常用Python第三方库简介
如果说强大的标准库奠定了Python发展的基石,丰富的第三方库则是python不断发展的保证,随着python的发展一些稳定的第三库被加入到了标准库里面,这里有6000多个第三方库的介绍 下表中加粗并 ...
- 『言善信』Fiddler工具 — 3、Fiddler界面布局详解【菜单栏】
目录 (一)Fiddler界面布局介绍 (二)菜单栏 1.File文件菜单: 2.Edit编辑菜单: 3.Rules规则菜单: 4.Tools工具菜单: 5.View视图菜单: 6.Help帮助菜单: ...
- Go语言web开发---Cronexpr 包实现并发定时任务
安装Cronexpr包: go get -u github.com/gorhill/cronexpr 这个包支持七位时间控制 *(秒) *(分) *(时) *(日) *(月) *(周) *(年) 栗子 ...
- TensorFlow实现多层感知机MINIST分类
TensorFlow实现多层感知机MINIST分类 TensorFlow 支持自动求导,可以使用 TensorFlow 优化器来计算和使用梯度.使用梯度自动更新用变量定义的张量.本文将使用 Tenso ...
- 基于TensorRT的BERT实时自然语言理解(上)
基于TensorRT的BERT实时自然语言理解(上) 大规模语言模型(LSLMs)如BERT.GPT-2和XL-Net为许多自然语言理解(NLU)任务带来了最先进的精准飞跃.自2018年10月发布以来 ...
- Spring:DI依赖注入的几种方式
据我所学,spring实现依赖注入(DI)的方式分为三大类:基于构造器(构造方法)的依赖注入.基于setter的依赖注入.其他方式(c命名空间.p命名空间等).其中推荐使用setter方法注入,这种注 ...
- 「题解」NWRRC2017 Grand Test
本文将同步发布于: 洛谷博客: csdn: 博客园: 简书. 题目 题目链接:洛谷 P7025.gym101612G. 题意概述 给你一张有 \(n\) 个点 \(m\) 条边的无向图,无重边无自环, ...
- Linkerd 2.10(Step by Step)—4. 如何配置外部 Prometheus 实例
Linkerd 2.10 系列 快速上手 Linkerd v2 Service Mesh(服务网格) 腾讯云 K8S 集群实战 Service Mesh-Linkerd2 & Traefik2 ...