MySQL:Innodb 唯一索引出現(xiàn)重復(fù)值的場(chǎng)景分析
最近遇到類似案例,這里將可能出現(xiàn)這種情況的2個(gè)場(chǎng)景描述一下,其中一種情況在翻看老葉的公眾號(hào)有類似文章,如下,
故障案例:MySQL唯一索引有重復(fù)值,官方卻說(shuō)This is not a bug
我們分別描述。
場(chǎng)景1 unique_checks = 0
1.原理
當(dāng)我們進(jìn)行數(shù)據(jù)插入的時(shí)候,對(duì)于唯一索引,實(shí)際上大概會(huì)經(jīng)歷數(shù)據(jù)查找,唯一性檢查、數(shù)據(jù)插入 3個(gè)階段。而對(duì)于普通索引來(lái)講如果page不在buffer pool中則可能在數(shù)據(jù)查找階段就會(huì)寫(xiě)入到ibuff,這種情況就等待后續(xù)的ibuff合并即可。
但是我們一旦設(shè)置了unique_checks=0,對(duì)于唯一索引(非主鍵)而言也可能走普通索引的方式,我們大概看看是如何改變的,首先根據(jù)設(shè)置,事務(wù)檢查唯一索引的標(biāo)記會(huì)設(shè)置為如下,
trx->check_unique_secondary =
!thd_test_options(thd, OPTION_RELAXED_UNIQUE_CHECKS);
然后在插入階段,row_ins_sec_index_entry_low函數(shù)首先會(huì)根據(jù)是否檢查唯一性將search_mode 設(shè)置上BTR_IGNORE_SEC_UNIQUE標(biāo)記,search_mode 的值很多,主要包含2類,A:讀寫(xiě)鎖模式/B:操作方式,他們各自占用不同的bit位。
if (!thr_get_trx(thr)->check_unique_secondary) {search_mode |= BTR_IGNORE_SEC_UNIQUE;}
接下來(lái)就是查找數(shù)據(jù)調(diào)用btr_cur_search_to_nth_level上層函數(shù),進(jìn)行數(shù)據(jù)定位,然后在其中判定,
case BTR_INSERT:
btr_op = (latch_mode & BTR_IGNORE_SEC_UNIQUE)
? BTR_INSERT_IGNORE_UNIQUE_OP
: BTR_INSERT_OP;
break;
如果為insert且latch_mode帶有BTR_IGNORE_SEC_UNIQUE,設(shè)置btr_op為BTR_INSERT_IGNORE_UNIQUE_OP。
最后在判定是否能夠使用ibuf上,我們看到如下,
if (btr_op != BTR_NO_OP &&
ibuf_should_try(index, btr_op != BTR_INSERT_OP)) { //是否進(jìn)入 ibuf
/* Try to buffer the operation if the leaf
page is not in the buffer pool. */
fetch = btr_op == BTR_DELETE_OP ? Page_fetch::IF_IN_POOL_OR_WATCH //這里和 WATCH和purge線程有光
: Page_fetch::IF_IN_POOL; //bug page get gen 只看是否在buffer中
}
而函數(shù)ibuf_should_try就是判定是否使用ibuf,一旦使用ibuf,當(dāng)然修改的相關(guān)page就不一定非要在buffer pool中,因此對(duì)于insert操作定義為Page_fetch::IF_IN_POOL,而函數(shù)ibuf_should_try主要包含如下判定:
- A:開(kāi)啟了change buffer
- B:不等于系統(tǒng)表空間
- C:不能是聚集索引
- D:不能處于export狀態(tài)下
- E:insert操作不能是唯一索引
- F:其他操作,唯一索引也可以使用ibuf,這里實(shí)際上就只剩下delete和ignore唯一性的insert了
而在底層修改操作實(shí)際上只有insert和delete操作,而這里滿足的是F條件因此insert操作的查找page動(dòng)作被標(biāo)記為Page_fetch::IF_IN_POOL,接下來(lái)buf_page_get_gen函數(shù)就不會(huì)再去訪問(wèn)物理磁盤了,這個(gè)時(shí)候可能返回的page為NULL,那就要走這個(gè)邏輯了:
if (block == nullptr) { //如果block沒(méi)有在innodb buffer中進(jìn)行操作
...
switch (btr_op) {
case BTR_INSERT_OP:
case BTR_INSERT_IGNORE_UNIQUE_OP: //注意這里
...
if (ibuf_insert(IBUF_OP_INSERT, tuple, index, page_id, page_size,
cursor->thr)) {
cursor->flag = BTR_CUR_INSERT_TO_IBUF;
goto func_exit;
}
也就是插入到ibuf中,那么我們可以想象,如果設(shè)置了unique_checks=0,這個(gè)時(shí)候如果重復(fù)的數(shù)據(jù)在磁盤上(因?yàn)閕nnodb buffer查詢不到page返回NULL),則會(huì)將接下來(lái)的數(shù)據(jù)本該重復(fù)的數(shù)據(jù)插入到ibuf,而不會(huì)去檢測(cè)重復(fù)值。然后等到需要讀取這個(gè)page到buffer pool的時(shí)候比如select,那就需要做ibuf的合并,合并后重復(fù)的數(shù)據(jù)就出現(xiàn)了。
2.測(cè)試
測(cè)試可以根據(jù)老葉公眾號(hào)的方式測(cè)試,主體思想就是做一個(gè)大一點(diǎn)的表,然后重啟數(shù)據(jù)庫(kù),并且不要開(kāi)啟啟動(dòng)時(shí)加載page到buffer pool,下面是我測(cè)試的結(jié)果:
這里b列是一個(gè)唯一索引,我們看到了第二查詢出現(xiàn)了2個(gè)相同的值。
3.其他和總結(jié)
當(dāng)出現(xiàn)這種情況的時(shí)候可以看到,第一個(gè)查詢只出現(xiàn)了一行,這看起來(lái)好像是對(duì)的,但是實(shí)際上索引上有2行不同的值,對(duì)于唯一索引來(lái)講如果訪問(wèn)到一行值,訪問(wèn)就會(huì)停止,因此出現(xiàn)了這種情況,看起來(lái)也是比較奇特。 因此我們?cè)诳紤]使用unique_checks=0加速導(dǎo)入數(shù)據(jù)的時(shí)候需要特別注意一下這個(gè)問(wèn)題,除非能夠保證數(shù)據(jù)都是唯一的否則不建議設(shè)置,現(xiàn)在我們知道實(shí)際上加速就是讓唯一索引也能夠使用ibuf這個(gè)特性,這里我們?cè)賮?lái)會(huì)看一下官方的這句話
For big tables, this saves a lot of disk I/O because InnoDB can use its change buffer to write secondary index records in a batch. Be certain that the data contains no duplicate keys.
很顯然和我們分析一致。
場(chǎng)景2 RR隔離級(jí)別相關(guān)
這個(gè)地方主要和隔離級(jí)別有關(guān)了,雖然提出這個(gè)BUG的時(shí)間有點(diǎn)久了,但是這不是BUG,并且8.0也能重現(xiàn),如下, https://bugs.mysql.com/bug.php?id=69979
重現(xiàn)如下:
建表和插入數(shù)據(jù)
create table testuniq(id int primary key,a int unique key);
insert into testuniq values (10, 100), (20, 200);
mysql> select * from testuniq;
+----+------+
| id | a |
+----+------+
| 10 | 100 |
| 20 | 200 |
+----+------+
TRX1 | TRX2 |
1.begin; | |
2.select * from testuniq; | |
3.update testuniq set a=300 where id=10; | |
4.update testuniq set a=100 where id=20; | |
5.select * from testuniq; |
完成第四步的時(shí)候數(shù)據(jù)就是:
mysql> select * from testuniq;
+----+------+
| id | a |
+----+------+
| 10 | 100 |
| 20 | 100 |
+----+------+
可以看到唯一索引出現(xiàn)了重復(fù)值,對(duì)于這個(gè)問(wèn)題,只要不阻止第4步的update testuniq set a=100 where id=20操作按照原理上來(lái)講就會(huì)出現(xiàn)重復(fù)值,因?yàn)镽R有一個(gè)read view在begin開(kāi)始后第一個(gè)select語(yǔ)句后一直存在,而update屬于當(dāng)前讀訪問(wèn)的當(dāng)前記錄已經(jīng)被修改了,因此第4步并沒(méi)有訪問(wèn)歷史記錄,因此update通過(guò),最終出現(xiàn)這種現(xiàn)象。同時(shí)在BUG中也詳細(xì)描述了這是符合設(shè)計(jì)的PG也是類似的結(jié)果,可以自行參考。