MySQL間隙鎖死鎖問題

語言: CN / TW / HK

點選進入“PHP開源社群”    

免費獲取進階面試、文件、影片資源

一、場景還原

當時同事A在線上程式碼中使用了Mybatis-plus的如下方法

com.baomidou.mybatisplus.extension.service.IService

saveOrUpdate(T, com.baomidou.mybatisplus.core.conditions.Wrapper<T>)

該方法先執行了update操作,如果更新到就不再執行後續操作,如果沒有更新到,才進行主鍵查詢,查詢到了就修改,未查詢到就新增。具體方法如下

/**
* <p>
* 根據updateWrapper嘗試更新,否繼續執行saveOrUpdate(T)方法
* 此次修改主要是減少了此項業務程式碼的程式碼量(存在性驗證之後的saveOrUpdate操作)
* </p>
*
* @param entity 實體物件
*/

default boolean saveOrUpdate(T entity, Wrapper<T> updateWrapper) {
return update(entity, updateWrapper) || saveOrUpdate(entity);
}

那麼這個方法的做法,為什麼會導致間隙鎖死鎖呢?咱們一起來分析並還原間隙鎖死鎖的場景。

二、什麼是間隙鎖

間隙鎖是MySQL行鎖的一種,與行鎖不同的是間隙鎖可能鎖定的是一行資料,也可能鎖住一個間隙。鎖定規則如下:

  • 當修改的資料存在時,間隙鎖只會鎖定當前行。

  • 當修改的資料不存在時,間隙鎖會向左找第一個比當前索引值小的值,向右找第一個比當前索引值大 的值(沒有則為正無窮),將此區間鎖住,從而阻止其他事務在此區間插入資料。

三、間隙鎖的作用

與行鎖(例如樂觀鎖高階實現,MVCC)組合成Next-key lock,在可重複讀這種隔離級別下一起工作避免幻讀。

四、如何關閉間隙鎖(強烈不建議關閉)

1、降低隔離級別,例如降為提交讀。

2、直接修改my.cnf,將開關,innodb_locks_unsafe_for_binlog改為1,預設為0即開啟

五、還原線上間隙鎖死鎖的場景

5.1 復現間隙鎖死鎖

5.1.1 我們先準備一個表

mysql> select * from t_gap_lock;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 張一 | 21 |
| 5 | 李五 | 25 |
| 6 | 趙六 | 26 |
| 9 | 王九 | 29 |
| 12 | 十二 | 12 |
+----+--------+------+

表中的id資料咱們準備了三個間隙:

  • 間隙一:1-5

  • 間隙二:6-9

  • 間隙三:12-正無窮

5.1.2 操作

1、此時我們開啟事務一,然後執行更新id=3的資料,按照咱們的理論,id=3這個資料不存在,說明它會在1-5之間加間隙鎖。

#開啟事務一
begin;

#事務一在1-5之間加間隙鎖
update t_gap_lock t set t.age = 23 where t.id = 3;

2、然後我們開啟事務二,然後執行更新id=7的資料,按照咱們的理論,id=7這個資料不存在,說明它會在6-9之間加間隙鎖

#開啟事務二
begin;

#事務二在6-9之間加間隙鎖
update t_gap_lock t set t.age = 27 where t.id = 7;

3、那麼重點來了,此時我們需要做的操作就是讓事務一在6-9之間插入資料,會發現此時事務已經被阻塞,無法執行insert,因為事務二已經對該區間加了間隙鎖。

#事務一在6-9之間插入資料
insert into t_gap_lock(id, name, age) values(8,'李八',28);

4、在事務一等待鎖的同時,咱們讓事務二同時在1-5之間插入資料,這個時候會發現,只要事務二一執行插入。MySQL立即報了死鎖,我們就會見到如下提示: [40001][1213] Deadlock found when trying to get lock; try restarting transaction

# 同時事務二在1-5之間插入資料
insert into t_gap_lock(id, name, age) values(3,'李三',23);

5.1.3 整個死鎖過程進行原理分析

1、首先事務一開啟事務後,更新id=3的資料,此資料不存在,所以事務一會鎖住1-5這個間隙,即為1-5這個間隙新增間隙鎖,同理,事務二會為6-9這個間隙新增間隙鎖;

2、然後我們讓事務一在6-9這個間隙插入資料,因為事務二已經加了間隙鎖,所以事務一需要等待事務二釋放間 隙鎖才能進行插入操作,此時事務一等待事務二釋放間隙鎖;

3、同理,事務二在1-5間隙插入時需要等待事務一釋放間隙鎖,兩個事務相互等待,死鎖產生。

那麼咱們此時就能大概明白最初那個Mybatis-plus的saveOrUpdate方法為什麼會造成間隙鎖死鎖的問題,也就是線上存在兩個併發事務,然後更新的時候都沒有更新到,此時都在自己的間隙加了間隙鎖,然後再到彼此的區間進行資料插入,此時就會造成兩個事務互相等待對方的釋放間隙鎖,從而導致死鎖。也許有同學會想,線上的資料幾乎不可能剛好會存在1-5,6-9這種間隙,來給併發事務各自加鎖,又剛好到彼此區間插入資料的場景,所以我們就會有接下來驗證間隙鎖加鎖是非互斥的,再一次深度還原間隙鎖死鎖的場景。

5.2 驗證間隙鎖加鎖非互斥

5.2.1 依然以t_gap_lock為例

mysql> select * from t_gap_lock;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 張一 | 21 |
| 5 | 李五 | 25 |
| 6 | 趙六 | 26 |
| 9 | 王九 | 29 |
| 12 | 十二 | 12 |
+----+--------+------+

5.2.2 操作

1、此時咱們開啟事務一,然後執行更新id=13的資料,按照咱們的理論,id=13這個資料不存在,說明它會在13-正無窮(因為當前索引樹上沒有比13更大的值)之間加間隙鎖。

#開啟事務一
begin;
#事務一在13-正無窮新增間隙鎖
update t_gap_lock t set t.age = 13 where t.id = 13;

2、然後我們開啟事務二,然後也執行更新id=13的資料,按照咱們的理論,事務二也會對13-正無窮之間加間隙鎖

#開啟事務二
begin;
#在13-正無窮新增間隙鎖
update t_gap_lock t set t.age = 13 where t.id = 13;

3、那麼重點來了,此時我們需要做的操作就是讓事務一在13-正無窮之間插入資料,會發現此時事務已經被阻塞,無法執行insert,因為事務二已經對該區間加了間隙鎖。

#事務一在13-正無窮中新增資料
insert into t_gap_lock(id, name, age) values (13,'十六',16);

4、在事務一等待鎖的同時,咱們讓事務二同時在13-正無窮之間插入資料,這個時候會發現,只要事務二一執行插入。MySQL立即報了死鎖,我們就會見到如下提示:

[40001][1213] Deadlock found when trying to get lock; try restarting transaction

#事務二在13-正無窮中新增資料
insert into t_gap_lock(id, name, age) values (13,'十六',16);

5、因為咱們已經用1-5以及6-9這種明顯的間隙還原了間隙鎖死鎖,所以13-正無窮髮生間隙鎖死鎖的原理與其無異,這裡有個非常大的區別就是事務一已經在13-正無窮加了間隙鎖,事務二依然可以對此間隙加間隙鎖,所以我們用實際證明了間隙鎖加鎖是非互斥的。此時咱們回憶一下Mybatis-plus的saveOrUpdate方法,發現線上只要出現兩個併發事務去修改同一條不存在的資料,就會立馬出現間隙鎖死鎖。

5.3 驗證當修改資料存在時,間隙鎖只會鎖住當前行

還有一個比較重要的點就是,當修改的資料存在時,MySQL只會鎖住當前行,咱們一起來分析下整個過程。

5.3.1 依然以t_gap_lock為例

mysql> select * from t_gap_lock;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | 張一 | 21 |
| 5 | 李五 | 25 |
| 6 | 趙六 | 26 |
| 9 | 王九 | 29 |
| 12 | 十二 | 12 |
+----+--------+------+

5.3.2 操作

1、此時我們開啟事務一,然後執行更新id=12的資料,按照咱們的理論,id=12這個資料存在,說明MySQL只會鎖定id=12這一行資料。

#開啟事務一
begin;
#事務一隻在12上加間隙鎖
update t_gap_lock t set t.age = 12 where t.id = 12;

2、然後我們開啟事務二,然後執行更新id=13的資料,按照咱們的理論,id=13這個資料不存在,說明它會在13-正無窮(因為當前索引樹上沒有比13更大的值)之間加間隙鎖

#開啟事務二
begin;
#事務二在13-正無窮新增間隙鎖
update t_gap_lock t set t.age = 13 where t.id = 13;

3、那麼重點來了,此時我們需要做的操作就是讓事務一在13-正無窮之間插入資料,會發現此時事務已經被阻塞,無法執行insert,因為事務二已經對該區間加了間隙鎖。

#事務一在13-正無窮中新增資料
insert into t_gap_lock(id, name, age) values (15,'十五',15);

4、在事務一等待鎖的同時,咱們讓事務二在12-正無窮之間插入資料,這個時候會發現,事務二能夠正常插入,說明事務二沒有被間隙鎖阻塞,待事務二提交或回滾後,事務一也正常提交。

#事務二在13-正無窮中新增資料
insert into t_gap_lock(id, name, age) values (13,'十六',16);

5、通過以上驗證,MySQL在更新id=12,即資料存在時,並沒有對12-正無窮新增間隙鎖,而是隻鎖定了id=12這一行資料,從而降低鎖的顆粒度以提高效能。

*宣告:本文於網路整理,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。

END

PHP開源社群

掃描關注  進入”PHP資料“

免費獲取進階

面試、文件、影片資源

點選“檢視原文”獲取更多