在MySQL的默認事務隔離級別——讀已提交(Read Committed, RC)中,開發者普遍認為不會出現間隙鎖(Gap Lock)。這一認知源于RC級別的設計原則:僅通過行鎖確保已提交數據的可見性,而將幻讀問題交由應用層處理。然而,近期多個生產環境卻報告了反常現象——RC級別下竟觸發了Gap Lock!這不僅導致事務意外阻塞,更動搖了我們對隔離級別底層行為的理解。
問題來源
“mysql RC 隔離級別也會出現gap 鎖嗎?我們生產環境是RC 隔離級別,出現了gap 鎖導致死鎖咯 ”? 上一周同事突如其來的問題徹底把我問懵了。
其實一直以來我也是認為只有RR隔離級別才會出現的,甚至我找到曾有人就這個問題給mysql 團隊提了個bug ,并且mysql 團隊也認為是個bug。那為啥現在RC 也會出現,甚至RU 級別也會出現呢。。我來盤下到底是怎么回事
問題背景
事情經過是這樣子的,上周我們生產環境報了個死鎖的問題,是并發執行insert?into?... on?duplicate?key?update 的時候報的一個死鎖。死鎖的日志是這樣子的
trx id 679250 lock_mode X locks gap before rec insert intention waiting
gap lock 阻塞了各自的的insert intention lock 。一眼看去這個是老生常談的死鎖現象了。慢著,我們生產環境是RC 隔離級別,一連串的疑問冒出來:RC 隔離級別怎么會出現gap lock 呢?gap lock 不是在RR隔離級別下為了解決幻讀而存在的嗎?直接顛覆了我的認知,不死心查幾次生產環境的事務隔離級別確認都是RC 隔離級別。
難道我記錯了嗎?登錄mysql官網,下面是官網的描述:
雖然說gap lock 可以通過改變為RC 進行禁用,但是依然在外鍵和唯一鍵的時候會用到gap lock。 um um 確實是官方說是會產生的,但是依然不明白為啥會用gap lock 呢?
網上找到一個網友跟我一樣,因為這個問題曾經給mysql 提了一個bug bug73170
神奇的是mysql 當時還當成bug 修復了,但是導致二級索引的唯一鍵失效,又revert 掉這個fix bug 68021
似乎看起來,mysql 團隊也曾經認為這是一個bug ,只是由于實現難度解決不了還是繼續使用了gap lock
Anway 先重現死鎖,再一步一步分析為啥唯一鍵的檢查需要用到gap lock,為什么mysql 團隊沒有去掉它
問題重現
根據我同事提供的重現sql.
第一步:
CREATE TABLE `test2` ( `id` int(11) NOT NULL AUTO_INCREMENT, `code` int(11) NOT NULL, `other` int(11) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `code` (`code`) ) ENGINE=InnoDB ; TRUNCATE table test2; insert ignore into test2 (id,code, other) values(1,1,1),(3,3,3),(5,5,5);
現在數據庫的數據是這樣子的
id | code | other |
---|---|---|
1 | 1 | 1 |
3 | 3 | 3 |
5 | 5 | 5 |
第二步session1?
begin; insert into test2(code, other) values (3, 4) on duplicate key update other = VALUES(other); 數據庫已經存在一個3的值,所以這里會沖突并且執行更新(這里的沖突很重要,是重現問題的關鍵)
第三步session2:
begin; insert into test2(code, other) values (5, 6) on duplicate key update other = VALUES(other); 數據庫已經存在一個5的值,所以這里會沖突并且執行更新(這里的沖突很重要,是重現問題的關鍵)
以上正常執行,但是問題來了,當我在session2 繼續插入(2,2)的時候,被鎖阻塞住了,why? 數據庫中有沒有這條記錄和我沖突,為啥要給我鎖住,而且我是RC 隔離級別,可以允許幻讀的存在,為啥給我加個gap lock?
session2: insert into test2(code, other) values (2, 2) on duplicate key update other = VALUES(other); 這個時候這個語句被阻塞了
第四步session1:
insert into test2(code, other) values (4, 4) on duplicate key update other = VALUES(other)
這個時候死鎖產生了,是不是覺得很奇怪,不管是session 2 插入的(2,2)還是session1 插入的(4,4)他們在數據庫中都不存在,理論上都應該可以正常執行,但實際上卻阻塞了,這不是導致并發性能下降了嗎?鎖粒度過大了呢?
執行show engine innodb status; 看下死鎖日志
從死鎖日志可以看到插入意向鎖是被對方的gap lock 阻塞住導致的死鎖
問題分析
二級唯一索引的
俗話說的好,你要解決一個問題,你得先去了解它,我們看看二級索引的唯一鍵是怎么實現的
find the B-tree page in the secondary index you want to insert the value to assert the B-tree page is latched equal-range = the range of records in the secondary index which conflict with your value if(equal-range is not empty){ release the latches on the B-tree and start a new mini-transaction for each record in equal-range lock gap before it, and the record itself (this is what LOCK_S does) also lock the gap after the last(equal-range) also (before Bug #32617942 was fixed) lock the record after last(equal-range) once you are done with all of the above, find the B-tree page again and latch it again } insert the record into the page and release the latch on the B-tree page.
可以看到在二級唯一索引插入record 的時候, 分成了兩個階段
- 判斷當前的物理記錄上是否有沖突的record(delete mark 為不沖突)
- 如果沒有沖突, 那么可以執行插入操作
這里在第一步 和 第二步 之間必須有鎖來保證, 否則第一步 判斷沒有沖突可以插入的時候, 但是在第一步和第二步 之間另外一個事務插入了一個沖突的record, 那么第二步 再插入的時候其實是沖突了.
所以當前的實現如果gap 上存在至少一個相同的record(包括刪除但是還沒被回收的記錄,因為刪除只是做了個刪除的墓碑標識,后面再回收), 那么需要給整個range 都加上gap X lock, 加了gap X lock 以后就可以禁止其他事務在這個gap 區間插入數據, 也就是通過lock 來保證第一步和第二步的原子性.
假設在code 這個唯一索引的數據是這樣子的,總共有兩個數據頁,page1通過point 指針指向下一個數據頁page2。紅色帶有delete mark 是代表這個數據已經被刪除了,由于還沒有給purge線程回收,因此還是在page 上,只是做了個刪除的墓碑標識。綠色代表是正常的數據
現在我們有兩個線程分別執行以下語句
sesson1: insert into test2(id,code, other) values (4,3, 4) on duplicate key update other = VALUES(other);
sesson2: insert into test2(id,code, other) values (12,3, 12) on duplicate key update other = VALUES(other);
session 1 執行第一個步判斷code 唯一索引上找到有相同value 的記錄 <3,3 delete mark>,<3,10 delete mark>,<3,11 delete mark>,<3,18 delete mark>,然后分別給他們加上next-key 鎖。
最后還得再<5,5> 上增加gap 鎖(假如這個5,5變成很大,那么意味著鎖的gap 會非常大,影響并發性能),以防止<3,19>之后的數據被插入。到這一步是沒有沖突的,因為這些值都是已經被刪除的,插進去后不會違背唯一索引
如果在session 1 在第一階段和第二階段中間,session2 并發執行,那么第一階段也會跟session 1 一樣執行,都加上gap lock。因此后面他們兩個人的插入都會失敗,成功避免他們都成功來導致最終的數據不一致的問題。當然也就引入了死鎖的
這個時候,你可能會說,如果我只是加數據上加鎖S的行鎖不是更好嗎?這樣就可以避免鎖的范圍太大,導致并發低下的問題。
session 1 執行第一個步判斷code 唯一索引上找到有相同value 的記錄 <3,3 delete mark>,<3,10 delete mark>,<3,11 delete mark>,<3,18 delete mark>,然后分別給他們加上s鎖。
session 2 并發執行,也分別給他們加上s 鎖,這個時候是不沖突的。他們兩個都認為數據庫沒有這個記錄,最后他們都插入成功。最終就違反了數據庫的唯一鍵規則,這也就是mysql 團隊修復了之前說bug 后帶來的問題,因此有立馬revert 了,直到今天都一直保留這個RC 隔離級別依然出現gap lock 的方式來保證唯一的正確性