当前位置: 首页 > 科技观察

有多少人被MySQL的这个BUG骗过?_0

时间:2023-03-16 20:44:28 科技观察

▌问题描述最近在线一个Mysql大客户的表从5.6升级到5.7后,在master上插入过程中出现“Duplicatekey”错误,主备和RO上都出现了实例。以其中一张表为例,迁移前通过“showcreatetable”命令查看到的自增id为1758609,迁移后变为1758598,迁移生成的新表中自增列的实际值是1758609,用户使用的是Innodb引擎,据运维同学反映,之前遇到过类似的问题,重启后即可恢复正常。▌内核问题排查由于用户反馈在5.6上访问正常,切换到5.7后会报错。所以,首先要怀疑是5.7内核有问题,所以第一反应是从官方bug列表中搜索类似问题,避免重复造车。搜索了一下,发现官方也有类似的bug,这里简单介绍下bug。背景知识1Innodb引擎中自增相关参数及数据结构主要参数包括:innodb_autoinc_lock_mode用于控制获取自增值的锁定方式,auto_increment_increment、auto_increment_offset用于控制自增的增量间隔和起始偏移量。递增列。涉及的主要结构包括:数据字典结构,保存了整个表的当前自增值和保护锁;事务结构,保存事务中处理的行数;handler结构体,保存了事务内多行的循环迭代信息。这部分网上有一篇文章介绍的比较好。详情参见:(https://www.cnblogs.com/zengkefu/p/5683258.html)。背景知识2mysql和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,tabletruncateprocessupdateautoincrement。(3)handler第一次打开时,会查询当前表中最大自增列的值,并用最大列的值加1初始化data_dict_t结构体中autoinc的值表的。(4)插入过程。autoinc修改相关的栈如下:ha_innobase::write_row:在write_row的第三步,调用handler句柄中的update_auto_increment函数更新autoincrement的值handler::update_auto_increment:调用Innodb接口获取一个自增自增值,根据当前auto_increment相关变量调整得到的自增值;同时设置当前handler要处理的下一个自增列的值。ha_innobase::get_auto_increment:获取dict_tabel中当前自增值,并根据全局参数更新下一个自增值到数据字典ha_innobase::dict_table_autoinc_initialize:更新自增值,如果指定值大于当前值,更新它。handler::set_next_insert_id:设置当前事务中要处理的下一行自增列的值。(5)更新行。对于“INSERTINTOt(c1,c2)VALUES(x,y)ONDUPLICATEKEYUPDATE”语句,无论唯一索引列指向的行是否存在,都需要自增的值先进的。相关代码如下:if(error==DB_SUCCESS&&table->next_number_field&&new_row==table->record[0]&&thd_sql_command(m_user_thd)==SQLCOM_INSERT&&trx->duplicates){ulonglongauto_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);...}从我们实际的业务流程来看,我们的错误可能只涉及插入和更新过程。BUG76872/88321:“InnoDBAUTO_INCREMENTproducessamevaluetwice”(一)Bug概述:当autoinc_lock_mode大于0且auto_increment_increment大于1时,多个线程对表插入操作时会产生“duplicatekey”错误同时系统重启后。(2)原因分析:重启后innodb会设置autoincrement的值为max(id)+1,此时在第一次插入时,write_row进程会调用handler::update_auto_increment设置autoinc相关信息。首先通过ha_innobase::get_auto_increment获取当前自增值(即max(id)+1),根据自增相关参数修改下一个自增值为next_id。当auto_increment_increment大于1时,max(id)+1不会大于next_id。handler::update_auto_increment获取到引擎层返回的值后,为了防止某些引擎在计算自增值时没有考虑当前的自增参数,会重新??计算当前的自增值row根据参数,因为Innodb内部考虑的是全局参数,所以handle层为Innodb返回的自增id计算的自增值也是next_id,自增id为next_id的行会是很快就插入了。handler层会根据write_row末尾当前行的next_id值设置下一个自增值。如果在write_row还没有设置表的下一个自增期间另一个线程也在执行插入过程,那么它得到的自增值也将是next_id。这会产生重复。(3)解决方案:在引擎内部获取自增列时考虑全局自增参数,使得重启后第一个插入线程获取的自增值不是max(id)+1,而是next_id,然后设置根据next_id值的下一个自动增量。由于这个过程是被锁保护的,所以其他线程再次获取autoincrement时就不会获取到重复的值了。通过上面的分析,只有当autoinc_lock_mode>0和auto_increment_increment>1时才会出现这个bug,在实际线上业务中,这两个参数都设置为1,所以可以排除这个bug导致线上问题的可能。▍现场分析和复现验证既然官方的BUG没能解决我们的问题,我们只好尽力从错误现象来分析。(1)分析maxid和autoincrement的规则由于用户的表设置了ONUPDATECURRENT_TIMESTAMP列,所以可以抓取所有错误表的maxid,autoincrement和最近更新的记录,看看有没有法律。抓取到的信息如下:乍一看,这个错误还是很有规律的。更新时间列是最后一次插入或修改的时间。结合autoincrement和maxid的值,好像最后一批事务只更新了行的auto-incrementid,并没有更新autoincrement的值。联想到【官方文档】中关于自增用法的介绍,update操作只能更新自增id,不能触发自增的推进。按照这个思路,我尝试着重现了用户的场景。复现方式如下:同时在binlog中,我们也看到了更新自增列的操作。如图:但是,由于binlog是ROW格式,我们无法判断是内核问题导致自增列发生变化,还是用户自己更新。所以我们联系客户确认,用户确定没有进行更新自增操作。那么这些自增列是从哪里来的呢?(2)分析用户的表和sql语句继续分析,发现用户有三类表(hz_notice_stat_sharding、hz_notice_group_stat_sharding、hz_freeze_balance_sharding),这三张表都有自增主键。但是除了hz_freeze_balance_sharding表,前两种都会出现autoinc错误。会不会是用户访问这两个表的方式不一样?抓取用户的sql语句,果然,前两张表使用了replaceinto操作,最后一张表使用了update操作。是replaceinto语句引起的问题吗?搜索官方bug,又发现了一个疑似bug。bug#87861:"Replaceintocausesmaster/slavehavedifferentauto_incrementoffsetvalues"原因:(1)mysql其实实现了replaceinto的delete+insert语句,但是在ROWbinlog格式下,会记录update类型的log到二进制日志。Insert语句会同步更新autoincrement,update不会。(2)replaceinto在Master上以delete+insert方式操作,自增正常。基于ROW格式复制到slave后,slave机器会按照update操作回放,只更新行中自增key的值,自增不更新。所以在slave机器上会出现max(id)大于autoincrement的情况。此时在ROW模式下,binlog记录了插入操作的所有列值,在slave上回放时不会重新分配自增id,所以不会报错。但是如果slave切换成master,遇到Insert操作就会出现“Duplicatekey”错误。(3)由于用户是从5.6迁移到5.7,然后直接在5.7上插入,相当于从slave上切割master,所以会报错。▍解决方案业务端可能的解决方案:(1)将binlog改为mixed或statement格式(2)ReplacereplaceintowithInsertonduplicatekeyupdate内核端可能的解决方案:(1)如果遇到replaceintoinROWformatstatement,以statement格式记录logevent,将原始statement记录到binlog。(2)将replaceinto语句的logevent记录为ROW格式的delete事件和insert事件。▍心得(1)修改autoincrement的autoinc_lock_mode和auto_increment_increment参数很容易导致重复key,所以在使用过程中尽量避免动态修改。(2)遇到线上问题,首先要做好现场分析,弄清楚故障发生的场景、用户的SQL语句、故障范围等,做好备份,以防过期。只有这样才能在找官方bug的时候准确匹配场景。如果官方没有BUG,我们也可以通过已有的线索独立分析。