MySQL:主从HASH SCAN算法可能导致从库数据错误

2023年 12月 30日 26.5k 0

本文主要以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中的下一条记录
    如果查找到对应的entry且比对成功,也就是entry不为NULL
      恢复record1到record0中
      并且删除hash 结构中的这个entry
      进行数据修改
   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中的数据,因此比较一定成功,进入修改流程。 这个时候实际上就是把表中的第一行数据给修改了。也就是这个时候数据已经不对了,再次进行修改在错误数据上进行修改自然就可能查不到数据的情况。这实际就是一个crc32碰撞后逻辑错误导致的问题。

五、总结

  • 数据量和本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修复的。
  • 8.0 hash scan 已经成为了默认的算法,因此概率大大提高。
  • 看来主键越来越重要了,有主键自然不会触发这个问题,还是重要事情说三遍吧,加主键、加主键、加主键。

相关文章

最新发布!MySQL 9.0 的向量 (VECTOR) 类型文档更新
国产数据库中级认证HCIP-openGauss经验分享
保障数据完整性与稳定性:数据库一致性
OceanBase 里的 DDL 超时时间
OceanBase v3.1.x 将不再更新版本 | 社区月报2024.6
openGauss Developer Day 2024 | SIG组工作会议亮点回看!

发布评论