本文主要以hash scan全表为基础进行分析,而不涉及到hash scan索引,实际上都会遇到这个问题。本文主要描述的是update event,delete event也是一样的,测试包含8022,8026,8028均包含这个问题。
约定:bi为update row event的before image
一、问题描述
这里简单看一下报错的我们直接用metalink 上的文章来看,实际上作为做oracle的老人,还是比较查metalink的,在metalink上也有一些MySQL相关的文章,但是很少,如下:
错误就是那个错误,解决办法也比较简单就是加上主键重做,这个问题我个人已经遇到N次了,每次都这么处理的,隐约的觉得hash scan 有BUG。
二、关于hash scan算法简介
在8.0中 hash scan 使用一个std::unordered_multimap的hash容器,记录其key - value值,每个key - value 代表修改的一行值,因为multimap容器允许重复的key - value,因此可以存在相同的行记录,这和5.7的实现不同,5.7是自己写的,而8.0 用的容器。其中
- key为当前表中根据每个字段计算出来的crc32值,句函数为Hash_slave_rows::make_hash_key,也就是checksum_crc32函数
- value为当前本行在event buffer中的位置,也就是指向实际的数据。
当然这里是简化了,实际value还包含一个std::unordered_multimap的迭代器和删除器,其中迭代器的作用是通过相同的key 调用,std::next 来查找下一个相同key的记录。
当一个event扫描结束后会将所有这个event的记录存储到这个hash容器中,函数Hash_slave_rows::put。而查找阶段会全表扫描本表,每次获取一行数据,然后在hash容器中进行查找,并进行处理,如下:
->循环1 读取表中的每条数据
计算本行数据的crc32值,并且在event的hash 结构查找对应的entry
->循环2
拷贝读取到的行从record0到record1,也就是record1为扫描到的行
->循环3
从查找的entry中获取bi记录的位置,并且放入到record0中
比对record0和record1的值是否相等,也就是record0是event对应的bi数据,而record1是扫描的本行数据
如果比对不成功这获取查找到entry key在event中的下一条记录
<-循环3结束条件为退出条件为找到了一条匹配记录或者entry为NULL
->如果查找到对应的entry且比对成功,也就是entry不为NULL
恢复record1到record0中
并且删除hash 结构中的这个entry
进行数据修改
<-循环2结束条件为再次使用record0也就是扫描的行在event的hash结构查找不到对应的entry,很显然后面逻辑只要匹配到了就会就会从event的hash结构查找中删除掉
这样做的目的很明确就是将全表扫描的次数减少,每个event才做一次,这样自然提高了性能。
三、BUG出场
这个BUG是同事查询到后给我的,BUG如下:
- https://bugs.mysql.combug/bug.php?id=101828
在这个BUG中,出现了2行记录crc32一致的情况,如下2个字符串的crc32也是一致的:
mysql> select crc32("b5a7b602ab754d7ab30fb42c4fb28d82");
+-------------------------------------------+
| crc32("b5a7b602ab754d7ab30fb42c4fb28d82") |
+-------------------------------------------+
| 2575120314 |
+-------------------------------------------+
1 row in set (3.16 sec)
mysql> select crc32("d19f2e9e82d14b96be4fa12b8a27ee9f");
+-------------------------------------------+
| crc32("d19f2e9e82d14b96be4fa12b8a27ee9f") |
+-------------------------------------------+
| 2575120314 |
+-------------------------------------------+
但是在整个hash scan 逻辑中,实际上比对crc32相同过后还是做了实际值的比较,也就是不完全依赖crc32值。这个BUG的流程如下:
- 第一阶段,数据准备阶段
CREATE TABLE t1 (
a bigint unsigned not null,
b bigint unsigned not null
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
insert into t1 values(0xa8e8ee744ced7ca8, 0x6850119e455ee4ed),(0x135cd25c170db910, 0x6916c5057592c796);
这两行数据的crc32值一样。
- 第二阶段,数据错误阶段
主库执行:
update t1 set a=1 where a=0x135cd25c170db910 and b=0x6916c5057592c796;
显然这条语句更改是第二行数据,但是到了从库由于BUG存在更改的是第一条数据,这个时候数据已经错误了。这个时候主库数据如下:
mysql> select * from t1;
+----------------------+---------------------+
| a | b |
+----------------------+---------------------+
| 12171240176243014824 | 7516527149547709677 |
| 1 | 7572456450708129686 |
+----------------------+---------------------+
2 rows in set (0.00 sec)
从库数据如下:
mysql> select * from t1;
+---------------------+---------------------+
| a | b |
+---------------------+---------------------+
| 1 | 7516527149547709677 |
| 1395221277543610640 | 7572456450708129686 |
+---------------------+---------------------+
2 rows in set (0.00 sec)
- 第三阶段,报错阶段
update t1 set a=2 where a=1;
这里主库修改是第二行记录,也就是
a=1
b=7572456450708129686
但是到了从库,因为a=1和b=7572456450708129686的hash crc32值和表中任何一个记录都不匹配 ,这报错。
四、原因
在上面的逻辑中来看一下为什么出现问题。
在例子中如果我们修改了1行数据,并且这行数据在表中有2数据存在相同的crc32值,主库修改的是2行数据,那么可能存在下面的问题:
从库首先在循环1中获取第1行数据,然后在hash结构中查找,找到相同的crc32值,进入循环2拷贝record后进入循环3首先对比record0到record1的值也就是event中的数据,也就是第2行数据和第1行数据对比,显然实际的值肯定不同,这获取event相同crc32的下一条记录,显然不存在因为就更改了1条数据,返回为NULL,循环3结束,继续,因为entry为NULL,恢复record0的操作和更改数据的操作都不会做。
然后循环2循环条件再次通过扫描到的行数据查找hash结构的entry依旧是第1行数据的entry,进行下一次循环,这个时候因为record0没有恢复,还是event对应的bi数据,因此拷贝后record1也就是event对应的bi数据,接着进入循环3,这个时候进行比较,实际上比较都是event中的数据,因此比较一定成功,进入修改流程。
这个时候实际上就是把表中的第一行数据给修改了。也就是这个时候数据已经不对了,再次进行修改在错误数据上进行修改自然就可能查不到数据的情况。
五、总结
- 数据量和本BUG相关,如果数据量大则crc32 不同记录产生相同CRC32的可能性就高一些。
- 本BUG一直未修复,BUG提交者提交了patch,实际上就是当entry为NULL的时候结束循环2,这样就会扫描表的下一条数据,而不是直接修改本行数据。不知道官方是否觉得BUG中提交的patch不合适,还是其他原因。
- 这个BUG看起来和Bug#28846386: RBR + STORED FUNCTION WITHOUT PRIMARY KEY - CAN'T FIND RECORD IN 有关,可能是修复一个BUG引入的新的BUG,这是8017修复的。
- 看来主键越来越重要了,有主键自然不会触发这个问题,还是重要事情说三遍吧,加主键、加主键、加主键。