往年这种状况,有时候不找好下家还真不敢跳,这不,前段时间刚跳到新东家,刚办入职那天,就遇上事了,真的是吓出一身冷汗(老大始终盯着我,说要疾速解决这个问题),差点被(背)开(锅)了....

状况如何?且听我上面缓缓道来!!!心愿对大家有所帮忙与借鉴。

问题形容

线上有个重要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拜访及批改的流程

  • (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_shardinghz_notice_group_stat_shardinghz_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,也能通过已有线索独立剖析。

作者:腾讯数据库技术
起源:http://r6e.cn/df8b