|MySQL的这个Bug,坑了多少人?( 二 )


(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及最近更新的几条记录抓取出来 , 看看是否有什么规律 。 抓取的信息如下:
|MySQL的这个Bug,坑了多少人?
本文插图

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

同时在binlog中 , 我们也看到有update自增列的操作 。 如图:
|MySQL的这个Bug,坑了多少人?
本文插图

不过 , 由于binlog是ROW格式 , 我们也无法判断这是内核出问题导致了自增列的变化还是用户自己更新所致 。 因此我们联系了客户进行确认 , 结果用户很确定没有进行更新自增列的操作 。
那么这些自增列到底是怎么来的呢?
(2) 分析用户的表及sql语句 继续分析 , 发现用户总共有三种类型的表(hz_notice_stat_sharding, hz_notice_group_stat_sharding,hz_freeze_balance_sharding) , 这三种表都有自增主键 。
但是前面两种都出现了autoinc错误 , 唯独hz_freeze_balance_sharding表没有出错 。


推荐阅读