1. 程式人生 > 資料庫 >MySQL-鎖-間隙鎖案例篇

MySQL-鎖-間隙鎖案例篇

間隙加鎖規則

  1. 原則1:加鎖的基本單位是next-key lock。希望你還記得,next-key lock是前開後閉區間。
  2. 原則2:查詢過程中訪問到的物件才會加鎖。
  3. 優化1:索引上的等值查詢,給唯一索引加鎖的時候,next-key lock退化為行鎖。
  4. 優化2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock退化為間隙鎖。
  5. 一個bug:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值為止。

注意!有行才會加行鎖。如果查詢條件沒有命中行,那就加next-key lock。當然,等值判斷的時候,需要加上優化2

(即:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,next-key lock退化為間隙鎖。)

<=到底是間隙鎖還是行鎖?其實,這個問題,你要跟“執行過程”配合起來分析。
在InnoDB要去找“第一個值”的時候,是按照等值去找的,用的是等值判斷的規則;
找到第一個值以後,要在索引內找“下一個值”,對應於我們規則中說的範圍查詢。

案例SQL

所有案例都是在可重複讀隔離級別(repeatable-read)下驗證的

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

案例1-等值查詢間隙鎖

由於表t中沒有id=7的記錄,所以用我們上面提到的加鎖規則判斷一下的話:

  1. 根據原則1,加鎖單位是next-key lock,session A加鎖範圍就是(5,10];
  2. 同時根據優化2,這是一個等值查詢(id=7),而id=10不滿足查詢條件,next-key lock退化成間隙鎖,因此最終加鎖的範圍是(5,10)。

所以,session B要往這個間隙裡面插入id=8的記錄會被鎖住,但是session C修改id=10這行是可以的。

案例2-非唯一索引等值鎖

這裡session A要給索引c上c=5的這一行加上讀鎖。

  1. 根據原則1,加鎖單位是next-key lock,因此會給(0,5]加上next-key lock。
  2. 要注意c是普通索引,因此僅訪問c=5這一條記錄是不能馬上停下來的,需要向右遍歷,查到c=10才放棄。根據原則2,訪問到的都要加鎖,因此要給(5,10]加next-key lock。
  3. 但是同時這個符合優化2:等值判斷,向右遍歷,最後一個值不滿足c=5這個等值條件,因此退化成間隙鎖(5,10)。
  4. 根據原則2 ,只有訪問到的物件才會加鎖,這個查詢使用覆蓋索引,並不需要訪問主鍵索引,所以主鍵索引上沒有加任何鎖,這就是為什麼session B的update語句可以執行完成。

但session C要插入一個(7,7,7)的記錄,就會被session A的間隙鎖(5,10)鎖住。

在這個例子中,lock in share mode只鎖覆蓋索引,但是如果是for update就不一樣了。 執行 for update時,系統會認為你接下來要更新資料,因此會順便給主鍵索引上滿足條件的行加上行鎖

**這個例子說明,鎖是加在索引上的;**同時,它給我們的指導是,如果你要用lock in share mode來給行加讀鎖避免資料被更新的話,就必須得繞過覆蓋索引的優化,在查詢欄位中加入索引中不存在的欄位。

所以,如果一個select * from … for update 語句,優化器決定使用全表掃描,那麼就會把主鍵索引上next-key lock全加上。

案例3:主鍵索引範圍鎖

mysql> select * from t where id=10 for update;
mysql> select * from t where id>=10 and id<11 for update;
這兩條查語句肯定是等價的,但是它們的加鎖規則不太一樣
  1. 開始執行的時候,要找到第一個id=10的行,因此本該是next-key lock(5,10]。 根據優化1, 主鍵id上的等值條件,退化成行鎖,只加了id=10這一行的行鎖。
  2. 範圍查詢就往後繼續找,找到id=15這一行停下來,因此需要加next-key lock(10,15]。

session A這時候鎖的範圍就是主鍵索引上,行鎖id=10和next-key lock(10,15]。這樣,session B和session C的結果你就能理解了。

這裡需要注意一點,首次session A定位查詢id=10的行的時候,是當做等值查詢來判斷的,而向右掃描到id=15的時候,用的是範圍查詢判斷。

案例4:非唯一索引範圍鎖

session A用欄位c來判斷,加鎖規則跟案例三唯一的不同是:在第一次用c=10定位記錄的時候,索引c上加了(5,10]這個next-key lock後,由於索引c是非唯一索引,沒有優化規則,也就是說不會蛻變為行鎖,因此最終sesion A加的鎖是,索引c上的(5,10] 和(10,15] 這兩個next-key lock

所以從結果上來看,sesson B要插入(8,8,8)的這個insert語句時就被堵住了。

這裡需要掃描到c=15才停止掃描,是合理的,因為InnoDB要掃到c=15,才知道不需要繼續往後找了。

案例5:唯一索引範圍鎖bug

session A是一個範圍查詢,按照原則1的話,應該是索引id上只加(10,15]這個next-key lock,並且因為id是唯一鍵,所以迴圈判斷到id=15這一行就應該停止了。但是實現上,InnoDB會往前掃描到第一個不滿足條件的行為止,也就是id=20。而且由於這是個範圍掃描,因此索引id上的(15,20]這個next-key lock也會被鎖上。

所以你看到了,session B要更新id=20這一行,是會被鎖住的。同樣地,session C要插入id=16的一行,也會被鎖住。

照理說,這裡鎖住id=20這一行的行為,其實是沒有必要的。因為掃描到id=15,就可以確定不用往後再找了,但實現上還是這麼做了

案例6:非唯一索引上存在"等值"的例子

insert into t values(30,10,30);

這時,session A在遍歷的時候,先訪問第一個c=10的記錄。同樣地,根據原則1,這裡加的是(c=5,id=5)到(c=10,id=10)這個next-key lock。然後,session A向右查詢,直到碰到(c=15,id=15)這一行,迴圈才結束。根據優化2,這是一個等值查詢,向右查詢到了不滿足條件的行,所以會退化成(c=10,id=10) 到 (c=15,id=15)的間隙鎖。

所以最後加鎖就是(5,5) 到(15,15)之間 即(c=5,id=5)和(c=15,id=15)這兩行上都沒有鎖。

案例7:limit 語句加鎖

session A的delete語句加了 limit 2。你知道表t裡c=10的記錄其實只有兩條,因此加不加limit 2,刪除的效果都是一樣的,但是加鎖的效果卻不同。可以看到,session B的insert語句執行通過了,跟案例六的結果不同。這是因為,案例七裡的delete語句明確加了limit 2的限制,因此在遍歷到(c=10, id=30)這一行之後,滿足條件的語句已經有兩條,迴圈就結束了。

因此,索引c上的加鎖範圍就變成了從(c=5,id=5)到(c=10,id=30)這個前開後閉區間;

案例8:一個死鎖的例子

現在,我們按時間順序來分析一下為什麼是這樣的結果。

  1. session A 啟動事務後執行查詢語句加lock in share mode,在索引c上加了next-key lock(5,10] 和間隙鎖(10,15);
  2. session B 的update語句也要在索引c上加next-key lock(5,10] ,進入鎖等待;
  3. 然後session A要再插入(8,8,8)這一行,被session B的間隙鎖鎖住。由於出現了死鎖,InnoDB讓session B回滾。

**其實是這樣的,session B的“加next-key lock(5,10] ”操作,實際上分成了兩步,先是加(5,10)的間隙鎖,加鎖成功;然後加c=10的行鎖,這時候才被鎖住的。**也就是說,我們在分析加鎖規則的時候可以用next-key lock來分析。但是要知道,具體執行的時候,是要分成間隙鎖和行鎖兩段來執行的。

案例9:order by改變加鎖方向

  1. 由於是order by c desc,第一個要定位的是索引c上“最右邊的”c=20的行,所以會加上間隙鎖(20,25)和next-key lock (15,20]。
  2. 在索引c上向左遍歷,要掃描到c=10才停下來,所以next-key lock會加到(5,10],這正是阻塞session B的insert語句的原因。
  3. 在掃描過程中,c=20、c=15、c=10這三行都存在值,由於是select *,所以會在主鍵id上加三個行鎖。

因此,session A 的select語句鎖的範圍就是:索引c上 (5, 25);主鍵索引上id=10、15、20三個行鎖。

這裡在說明一下!鎖就是加在索引上的,這是InnoDB的一個基礎設定,需要你在分析問題的時候要一直記得。

案例10:再次分析範圍查詢

begin;
select * from t where id>9 and id<12 order by id desc for update;

我們知道這個語句的加鎖範圍是主鍵索引上的 (0,5]、(5,10]和(10, 15)。也就是說,id=15這一行,並沒有被加上行鎖

我們說加鎖單位是next-key lock,都是前開後閉區間,但是這裡用到了優化2,即索引上的等值查詢,向右遍歷的時候id=15不滿足條件,所以next-key lock退化為了間隙鎖 (10, 15)。

  1. 首先這個查詢語句的語義是order by id desc,要拿到滿足條件的所有行,優化器必須先找到“第一個id<12的值”。
  2. 這個過程是通過索引樹的搜尋過程得到的,在引擎內部,其實是要找到id=12的這個值,只是最終沒找到,但找到了(10,15)這個間隙。
  3. 然後向左遍歷,在遍歷過程中,就不是等值查詢了,會掃描到id=5這一行,所以會加一個next-key lock (0,5]。
begin;
select id from t where c in(5,20,10) lock in share mode;
  1. 在查詢c=5的時候,先鎖住了(0,5]。但是因為c不是唯一索引,為了確認還有沒有別的記錄c=5,就要向右遍歷,找到c=10才確認沒有了,這個過程滿足優化2,所以加了間隙鎖(5,10)。
  2. 同樣的,執行c=10這個邏輯的時候,加鎖的範圍是(5,10] 和 (10,15);執行c=20這個邏輯的時候,加鎖的範圍是(15,20] 和 (20,25)。4
  3. 通過這個分析,我們可以知道,這條語句在索引c上加的三個記錄鎖的順序是:先加c=5的記錄鎖,再加c=10的記錄鎖,最後加c=20的記錄鎖。

需要注意的 select id from t where c in(5,20,10) order by c desc for update; 間隙鎖雖然不互斥間隙鎖,但是細化到記錄上還是會出現互斥的。問題是in條件的加鎖時一個個去掃描的。所以兩個sql語句在併發時有可能會出現死鎖

怎麼檢視死鎖

下面試間隙鎖案例10出現死鎖情況

在出現死鎖後,執行show engine innodb status命令得到的部分輸出。這個命令會輸出很多資訊,有一節LATESTDETECTED DEADLOCK,就是記錄的最後一次死鎖資訊。
  1. 這個結果分成三部分:
    • TRANSACTION,是第一個事務的資訊;
    • TRANSACTION,是第二個事務的資訊;
    • WE ROLL BACK TRANSACTION (1),是最終的處理結果,表示回滾了第一個事務。
  2. 第一個事務的資訊中:
    • WAITING FOR THIS LOCK TO BE GRANTED,表示的是這個事務在等待的鎖資訊;
    • index c of table test.t,說明在等的是表t的索引c上面的鎖;
    • lock mode S waiting 表示這個語句要自己加一個讀鎖,當前的狀態是等待中;
    • Record lock說明這是一個記錄鎖;
    • n_fields 2表示這個記錄是兩列,也就是欄位c和主鍵欄位id;
    • 0: len 4; hex 0000000a; asc ;;是第一個欄位,也就是c。值是十六進位制a,也就是10;
    • 1: len 4; hex 0000000a; asc ;;是第二個欄位,也就是主鍵id,值也是10;
    • 這兩行裡面的asc表示的是,接下來要打印出值裡面的“可列印字元”,但10不是可列印字元,因此就顯示空格。
    • 第一個事務資訊就只顯示出了等鎖的狀態,在等待(c=10,id=10)這一行的鎖。
    • 當然你是知道的,既然出現死鎖了,就表示這個事務也佔有別的鎖,但是沒有顯示出來。彆著急,我們從第二個事務的資訊中推匯出來。
  3. 第二個事務顯示的資訊要多一些:
    • “ HOLDS THE LOCK(S)”用來顯示這個事務持有哪些鎖;
    • index c of table test.t 表示鎖是在表t的索引c上;
    • hex 0000000a和hex 00000014表示這個事務持有c=10和c=20這兩個記錄鎖;
    • WAITING FOR THIS LOCK TO BE GRANTED,表示在等(c=5,id=5)這個記錄鎖。

從上面這些資訊中,我們就知道:

  1. “lock in share mode”的這條語句,持有c=5的記錄鎖,在等c=10的鎖;
  2. “for update”這個語句,持有c=20和c=10的記錄鎖,在等c=5的記錄鎖。

因此導致了死鎖。這裡,我們可以得到兩個結論:

  1. 由於鎖是一個個加的,要避免死鎖,對同一組資源,要按照儘量相同的順序訪問;
  2. 在發生死鎖的時刻,for update 這條語句佔有的資源更多,回滾成本更大,所以InnoDB選擇了回滾成本更小的lock in share mode語句,來回滾。

怎麼看鎖等待

可以看到,由於session A並沒有鎖住c=10這個記錄,所以session B刪除id=10這一行是可以的。但是之後,session B再想insert id=10這一行回去就不行了。

show engine innodb status

  1. index PRIMARY of table test.t ,表示這個語句被鎖住是因為表t主鍵上的某個鎖。
  2. lock_mode X locks gap before rec insert intention waiting 這裡有幾個資訊:
    • insert intention表示當前執行緒準備插入一個記錄,這是一個插入意向鎖。為了便於理解,你可以認為它就是這個插入動作本身。
    • gap before rec 表示這是一個間隙鎖,而不是記錄鎖。
  3. 那麼這個gap是在哪個記錄之前的呢?接下來的0~4這5行的內容就是這個記錄的資訊。
  4. n_fields 5也表示了,這一個記錄有5列:
    • 0: len 4; hex 0000000f; asc ;;第一列是主鍵id欄位,十六進位制f就是id=15。所以,這時我們就知道了,這個間隙就是id=15之前的,因為id=10已經不存在了,它表示的就是(5,15)。
    • 1: len 6; hex 000000000513; asc ;;第二列是長度為6位元組的事務id,表示最後修改這一行的是trx id為1299的事務。
    • 2: len 7; hex b0000001250134; asc % 4;; 第三列長度為7位元組的回滾段資訊。可以看到,這裡的acs後面有顯示內容(%和4),這是因為剛好這個位元組是可列印字元。
    • 後面兩列是c和d的值,都是15。

因此,我們就知道了,由於delete操作把id=10這一行刪掉了,原來的兩個間隙(5,10)、(10,15)變成了一個(5,15)。

session A執行完select語句後,什麼都沒做,但它加鎖的範圍突然“變大”了;

當我們執行select * from t where c>=15 and c<=20 order by c desc lock in share mode; 向左掃描到c=10的時候,要把(5, 10]鎖起來。

也就是說,所謂“間隙”,其實根本就是由“這個間隙右邊的那個記錄”定義的

update間隙鎖變大的例子

session A的加鎖範圍是索引c上的 (5,10]、(10,15]、(15,20]、(20,25]和(25,suprenum]。

之後session B的第一個update語句,要把c=5改成c=1,你可以理解為兩步:

  1. 插入(c=1, id=5)這個記錄;
  2. 刪除(c=5, id=5)這個記錄。

按照我們上面說的,索引c上(5,10)間隙是由這個間隙右邊的記錄。所以通過這個操作,session A間隙鎖範圍變成下層

接下來session B要執行 update t set c = 5 where c = 1這個語句了,一樣地可以拆成兩步:

  1. 插入(c=5, id=5)這個記錄;
  2. 刪除(c=1, id=5)這個記錄。

第一步試圖在已經加了間隙鎖的(1,10)中插入資料,所以就被堵住了。

kill鎖住的執行緒

 select * from t where id=1; # 長時間沒返回

分析語句之前 都應該執行show processlist;檢視 後續操作找到了blocking_id kill掉

長時間沒返回

如果:performance_schema=on 開啟著可以通過select blocking_pid from sys.schema_table_lock_waits; 

行鎖

 select * from t sys.innodb_lock_waits where locked_table=`'test'.'t'
 ## 查詢指定表裡面的情況
select * from t where c=5 for update
當級別為RR時,是可以解決幻讀的,此時對於每條記錄的間隙還要加上GAP鎖。也就是說,表上每一條記錄和每一個間隙都鎖上了。
但是實際上 innodb先鎖全表的所有行 返回server層,InnoDB就會把不滿足條件的行行鎖去掉。所以語句執行完只會鎖c=5的行。

小總結

  1. 表鎖是主動在SQL 上面加的

  2. MDL鎖時自動加比如改變表結構

  3. 如果在一個事務內,更新的資料條數過多。建議分開更新,因為在你事務開啟時,其他session的查詢都會查詢你的undo log鏈條,如果你的鏈條太長會導致很慢

  4. (DML、DDL語句時都會申請MDL鎖,DML操作需要MDL讀鎖,DDL操作需要MDL寫鎖)

te
當級別為RR時,是可以解決幻讀的,此時對於每條記錄的間隙還要加上GAP鎖。也就是說,表上每一條記錄和每一個間隙都鎖上了。
但是實際上 innodb先鎖全表的所有行 返回server層,InnoDB就會把不滿足條件的行行鎖去掉。所以語句執行完只會鎖c=5的行。




### 小總結

1. 表鎖是主動在SQL 上面加的

2. MDL鎖時自動加比如改變表結構

3. **如果在一個事務內,更新的資料條數過多。建議分開更新,因為在你事務開啟時,其他session的查詢都會查詢你的undo log鏈條,如果你的鏈條太長會導致很慢**。

4. (DML、DDL語句時都會申請MDL鎖,DML操作需要MDL讀鎖,DDL操作需要MDL寫鎖)

   > 增刪改查,或者修改表之類的語句都會申請MDL鎖,而增刪改查 會自動加上MDL讀鎖。修改表會加上寫鎖