今年这种情况,有时候不找好下家还真不敢跳,这不,前段时间刚跳到新东家,刚办入职那天,就遇上事了,真的是吓出一身冷汗(老大一直盯着我,说要快速解决这个问题),差点被(背)开(锅)了....
情况如何?且听我下面慢慢道来!!!希望对大家有所帮助与借鉴。
问题描述
线上有个重要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结构体,保存事务内部多行的循环迭代信息。
背景知识2
mysql及Innodb引擎中对autoincrement访问及修改的流程
- 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:设置当前事务中下一个要处理的行的自增列的值。
- 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"
此时,首次插入时,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。这样就产生了重复。
通过上述分析,这个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”
原因:
因此在slave机上就会出现max(id)大于autoincrement的情况。此时在ROW模式下对于insert操作binlog记录了所有的列的值,在slave上回放时并不会重新分配自增id,因此不会报错。但是如果slave切master,遇到Insert操作就会出现”Duplicate key”的错误。
解决方案
业务侧的可能解决方案:
内核侧可能解决方案:
心得
只有这样才能在找官方bug时精准的匹配场景,如果官方没有相关bug,也能通过已有线索独立分析。
详解Spring Controller autowired Request变量 spring的DI大家比较熟悉了,对于...
在Asp.net Core之前所有的Action返回值都是ActionResult,Json(),File()等方法返...
最近在学习jQuery时接触到了show()、hide()、toggle()函数,于是利用这几个函数...
大家好我是爱景甜的网工我是一个思科出身专注于华为的网工 好了话不多说进入正题...
多年以后,面对台下五彩斑斓的Jetbrain和Vscode用户,这位曾经的资深的vim追随者...
在新的MySQL 8.0.23中,引入了新的有趣功能:不可见列。 这是第一篇关于这个新功...
git clone支持https和git(即ssh)两种方式下载源码: 当使用git方式下载时,如...
一个常见的场景,获取:标签背景图片链接: 如字符串:var bgImg = "url (\" htt...
1 概述 在接下来的时间里,将会入手ASP.NET MVC这一专题,尽量用最快的时间,最...
需要注意的是,调用的封装的数据库,和jQuery的保存地址 一、注册 (1)写文本框...