共计 4856 个字符,预计需要花费 13 分钟才能阅读完成。
这篇文章主要介绍“MySQL 中的 BUG 分析”,在日常操作中,相信很多人在 MySQL 中的 BUG 分析问题上存在疑惑,丸趣 TV 小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”MySQL 中的 BUG 分析”的疑惑有所帮助!接下来,请跟着丸趣 TV 小编一起来学习吧!
▌问题描述
近期,线上有个重要 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; hellip; hellip; 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); hellip; hellip; }
从我们的实际业务流程来看,我们的错误只可能涉及 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。
到此,关于“MySQL 中的 BUG 分析”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注丸趣 TV 网站,丸趣 TV 小编会继续努力为大家带来更多实用的文章!