MySQL:主從HASH SCAN算法可能導(dǎo)致從庫(kù)數(shù)據(jù)錯(cuò)誤
本文主要以hash scan全表為基礎(chǔ)進(jìn)行分析,而不涉及到hash scan索引,實(shí)際上都會(huì)遇到這個(gè)問(wèn)題。本文主要描述的是update event,delete event也是一樣的,測(cè)試包含8022,8026,8028均包含這個(gè)問(wèn)題。
約定:bi為update row event的before image
一、問(wèn)題描述
這里簡(jiǎn)單看一下報(bào)錯(cuò)的我們直接用metalink 上的文章來(lái)看,實(shí)際上作為做oracle的老人,還是比較查metalink的,在metalink上也有一些MySQL相關(guān)的文章,但是很少,如下:
錯(cuò)誤就是那個(gè)錯(cuò)誤,解決辦法也比較簡(jiǎn)單就是加上主鍵重做,這個(gè)問(wèn)題我個(gè)人已經(jīng)遇到N次了,每次都這么處理的,隱約的覺(jué)得hash scan 有BUG。
二、關(guān)于hash scan算法簡(jiǎn)介
在8.0中 hash scan 使用一個(gè)std::unordered_multimap的hash容器,記錄其key - value值,每個(gè)key - value 代表修改的一行值,因?yàn)閙ultimap容器允許重復(fù)的key - value,因此可以存在相同的行記錄,這和5.7的實(shí)現(xiàn)不同,5.7是自己寫(xiě)的,而8.0 用的容器。其中:
- key為當(dāng)前表中根據(jù)每個(gè)字段計(jì)算出來(lái)的crc32值,句函數(shù)為Hash_slave_rows::make_hash_key,也就是checksum_crc32函數(shù)
- value為當(dāng)前本行在event buffer中的位置,也就是指向?qū)嶋H的數(shù)據(jù)。
當(dāng)然這里是簡(jiǎn)化了,實(shí)際value還包含一個(gè)std::unordered_multimap的迭代器和刪除器,其中迭代器的作用是通過(guò)相同的key 調(diào)用,std::next 來(lái)查找下一個(gè)相同key的記錄。
當(dāng)一個(gè)event掃描結(jié)束后會(huì)將所有這個(gè)event的記錄存儲(chǔ)到這個(gè)hash容器中,函數(shù)Hash_slave_rows::put。而查找階段會(huì)全表掃描本表,每次獲取一行數(shù)據(jù),然后在hash容器中進(jìn)行查找,并進(jìn)行處理,如下:
->循環(huán)1 讀取表中的每條數(shù)據(jù)
計(jì)算本行數(shù)據(jù)的crc32值,并且在event的hash 結(jié)構(gòu)查找對(duì)應(yīng)的entry
->循環(huán)2
拷貝讀取到的行從record0到record1,也就是record1為掃描到的行
->循環(huán)3
從查找的entry中獲取bi記錄的位置,并且放入到record0中
比對(duì)record0和record1的值是否相等,也就是record0是event對(duì)應(yīng)的bi數(shù)據(jù),而record1是掃描的本行數(shù)據(jù)
如果比對(duì)不成功這獲取查找到entry key在event中的下一條記錄
<-循環(huán)3結(jié)束條件為退出條件為找到了一條匹配記錄或者entry為NULL
->如果查找到對(duì)應(yīng)的entry且比對(duì)成功,也就是entry不為NULL
恢復(fù)record1到record0中
并且刪除hash 結(jié)構(gòu)中的這個(gè)entry
進(jìn)行數(shù)據(jù)修改
<-循環(huán)2結(jié)束條件為再次使用record0也就是掃描的行在event的hash結(jié)構(gòu)查找不到對(duì)應(yīng)的entry,很顯然后面邏輯只要匹配到了就會(huì)就會(huì)從event的hash結(jié)構(gòu)查找中刪除掉
這樣做的目的很明確就是將全表掃描的次數(shù)減少,每個(gè)event才做一次,這樣自然提高了性能。
三、BUG登場(chǎng)
這個(gè)BUG是同事查詢到后給我的,BUG如下:https://bugs.mysql.com/bug.php?id=101828
在這個(gè)BUG中,出現(xiàn)了2行記錄crc32一致的情況,如下2個(gè)字符串的crc32也是一致的:
mysql> select crc32("b5a7b602ab754d7ab30fb42c4fb28d82");
+-------------------------------------------+
| crc32("b5a7b602ab754d7ab30fb42c4fb28d82") |
+-------------------------------------------+
| 2575120314 |
+-------------------------------------------+
1 row in set (3.16 sec)
mysql> select crc32("d19f2e9e82d14b96be4fa12b8a27ee9f");
+-------------------------------------------+
| crc32("d19f2e9e82d14b96be4fa12b8a27ee9f") |
+-------------------------------------------+
| 2575120314 |
+-------------------------------------------+
但是在整個(gè)hash scan 邏輯中,實(shí)際上比對(duì)crc32相同過(guò)后還是做了實(shí)際值的比較,也就是不完全依賴crc32值。這個(gè)BUG的流程如下:
第一階段,數(shù)據(jù)準(zhǔn)備階段:
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);
這兩行數(shù)據(jù)的crc32值一樣。
第二階段,數(shù)據(jù)錯(cuò)誤階段 主庫(kù)執(zhí)行:
update t1 set a=1 where a=0x135cd25c170db910 and b=0x6916c5057592c796;
顯然這條語(yǔ)句更改是第二行數(shù)據(jù),但是到了從庫(kù)由于BUG存在更改的是第一條數(shù)據(jù),這個(gè)時(shí)候數(shù)據(jù)已經(jīng)錯(cuò)誤了。這個(gè)時(shí)候主庫(kù)數(shù)據(jù)如下:
mysql> select * from t1;
+----------------------+---------------------+
| a | b |
+----------------------+---------------------+
| 12171240176243014824 | 7516527149547709677 |
| 1 | 7572456450708129686 |
+----------------------+---------------------+
2 rows in set (0.00 sec)
從庫(kù)數(shù)據(jù)如下:
mysql> select * from t1;
+---------------------+---------------------+
| a | b |
+---------------------+---------------------+
| 1 | 7516527149547709677 |
| 1395221277543610640 | 7572456450708129686 |
+---------------------+---------------------+
2 rows in set (0.00 sec)
第三階段,報(bào)錯(cuò)階段:
update t1 set a=2 where a=1;
這里主庫(kù)修改是第二行記錄,也就是:
a=1
b=7572456450708129686
但是到了從庫(kù),因?yàn)閍=1和b=7572456450708129686的hash crc32值和表中任何一個(gè)記錄都不匹配 ,這報(bào)錯(cuò)。
四、原因
來(lái)看一下為什么出現(xiàn)問(wèn)題。 在例子中如果我們修改了1行數(shù)據(jù),并且這行數(shù)據(jù)在表中有2數(shù)據(jù)存在相同的crc32值,主庫(kù)修改的是2行數(shù)據(jù),那么可能存在下面的問(wèn)題:
從庫(kù)首先在循環(huán)1中獲取第1行數(shù)據(jù),然后在hash結(jié)構(gòu)中查找,找到相同的crc32值,進(jìn)入循環(huán)2拷貝record后進(jìn)入循環(huán)3首先對(duì)比record0到record1的值也就是event中的數(shù)據(jù),也就是第2行數(shù)據(jù)和第1行數(shù)據(jù)對(duì)比,顯然實(shí)際的值肯定不同,這獲取event相同crc32的下一條記錄,顯然不存在因?yàn)榫透牧?條數(shù)據(jù),返回為NULL,循環(huán)3結(jié)束,繼續(xù),因?yàn)閑ntry為NULL,恢復(fù)record0的操作和更改數(shù)據(jù)的操作都不會(huì)做。
然后循環(huán)2循環(huán)條件再次通過(guò)掃描到的行數(shù)據(jù)查找hash結(jié)構(gòu)的entry依舊是第1行數(shù)據(jù)的entry,進(jìn)行下一次循環(huán),這個(gè)時(shí)候因?yàn)閞ecord0沒(méi)有恢復(fù),還是event對(duì)應(yīng)的bi數(shù)據(jù),因此拷貝后record1也就是event對(duì)應(yīng)的bi數(shù)據(jù),接著進(jìn)入循環(huán)3,這個(gè)時(shí)候進(jìn)行比較,實(shí)際上比較都是event中的數(shù)據(jù),因此比較一定成功,進(jìn)入修改流程。 這個(gè)時(shí)候?qū)嶋H上就是把表中的第一行數(shù)據(jù)給修改了。也就是這個(gè)時(shí)候數(shù)據(jù)已經(jīng)不對(duì)了,再次進(jìn)行修改在錯(cuò)誤數(shù)據(jù)上進(jìn)行修改自然就可能查不到數(shù)據(jù)的情況。這實(shí)際就是一個(gè)crc32碰撞后邏輯錯(cuò)誤導(dǎo)致的問(wèn)題。
五、總結(jié)
- 數(shù)據(jù)量和本BUG相關(guān),如果數(shù)據(jù)量大則crc32 不同記錄產(chǎn)生相同crc32的可能性就高一些。
- 本BUG一直未修復(fù),BUG提交者提交了patch,實(shí)際上就是當(dāng)entry為NULL的時(shí)候結(jié)束循環(huán)2,這樣就會(huì)掃描表的下一條數(shù)據(jù),而不是直接修改本行數(shù)據(jù)。不知道官方是否覺(jué)得BUG中提交的patch不合適,還是其他原因。
- 這個(gè)BUG看起來(lái)和Bug#28846386: RBR + STORED FUNCTION WITHOUT PRIMARY KEY - CAN'T FIND RECORD IN 有關(guān),可能是修復(fù)一個(gè)BUG引入的新的BUG,這是8017修復(fù)的。
- 8.0 hash scan 已經(jīng)成為了默認(rèn)的算法,因此概率大大提高。
- 看來(lái)主鍵越來(lái)越重要了,有主鍵自然不會(huì)觸發(fā)這個(gè)問(wèn)題,還是重要事情說(shuō)三遍吧,加主鍵、加主鍵、加主鍵。