本文主要總結 MySQL 事務幾種隔離級別的實現和其中鎖的使用情況。因為 Mysql 幾種存儲引擎中 InnoDB 使用的最多,同時也支持事務和鎖,所以這篇主要說得是 InnoDB 引擎下的鎖機制與事務。
在開始前先簡單回顧事務幾種隔離級別以及帶來的問題。
四種隔離級別:讀未提交、讀已提交、可重復讀、可串行化。
帶來的問題:臟讀、不可重復讀、幻讀。分別是由讀未提交、讀已提交、可重復讀引起的。
臟讀:一個事務讀取到在另一個事務還未提交時的修改。
不可重復讀:一個事務在另一個事務提交前后讀取到了不同數據。(側重於某一條數據,這條數據內容發生了變化)。
幻讀:一個事務在另一個事務提交前后讀取到了不同數據。(側重於多了或是少了一條數據)。
在 Mysql 中,默認隔離級別是可重復讀,在默認時卻一定程度上解決了幻讀,為什么這么說呢?請看下面這個例子。
同時我們查看數據庫中的數據:
可以看到並沒有發生 “幻讀”,這是為什么?難道可重復讀級別已經解決了“幻讀”?后面會詳細解釋。
Mysql 中的鎖
對於存儲引擎 MyISAM ,只支持表級鎖,對於 InnoDB 來說,既支持表級鎖、也支持行級鎖。所以 InnoDB 可以用於高並發的場景下而 MyISAM 不行。
按顆粒度划分
1、行級鎖
只對一行數據加鎖,當一個事務操作某一行事務時,只對該行數據加排他鎖時,其他事務對其他行數據操作時不會影響,並發性好。缺點是在加多條數據時加鎖會比較耗時。一個事務獲取到鎖后直到事務提交才會釋放鎖。
使用場景:可串行化隔離級別
2、表級鎖
包含兩種。1、MDL,是 DDL 操作與 DML、DQL 操作沖突的加鎖。下面會講解。
2、對整張表進行加讀寫鎖,僅針對 DML、DQL 操作。加鎖快但是可承受的並發量低。 加讀鎖:lock table 表名 read ; 加寫鎖:lock tables 表名 write ; 釋放讀寫鎖:unlock tables;
3、全局鎖
對所有表所有數據進行加鎖。這個鎖是讀鎖,也就是加鎖后當前數據庫只能處理讀操作,不能處理寫操作。一般在將整個庫的數據進行邏輯備份時使用(在 InnoDB 中可以使用 mysqldump 進行非阻塞式備份,原理就是通過 隔離級別MVCC數據一致性實現的)。 加鎖: Flush tables with read lock (FTWRL)
這里不建議使用 set global readonly = true 來代替 FTWRL,因為 1、readonly 參數在某些系統業務中是用來判斷當前庫是主庫還是從庫,如果修改會對業務操作影響 2、在發生異常時 readonly 不會改變而 FTWRL 會自動釋放鎖,未改變的話在恢復后就無法處理寫操作,導致系統出錯。
4、頁級鎖(存儲引擎BDB,不常使用)
對一頁數據進行加鎖,介於行級鎖與表級鎖之間。
按種類划分
1、共享鎖(讀鎖)
共享鎖是對於MySQL中的讀操作的,所以共享鎖也叫讀鎖,一個事務進行讀操作時,會對讀取的數據添加讀鎖(可串行化下的讀操作是自動加鎖的,其他隔離級別需要在查詢語句后面添加 lock in share mode),加鎖后其他事務也可以對加鎖的數據進行讀取。獲取了某記錄的共享鎖后只能對其進行讀取,不能修改,也不能去讀取其他表的數據
2、排他鎖(寫鎖)
排它鎖是對於 MySQL 中的寫操作的,所以排它鎖也叫寫鎖。添加排它鎖的數據其他事務就不能進行操作,同時共享鎖與排它鎖也是互斥的,也就是一個事務對某數據添加了共享鎖,那么其他事務就不能對其再添加排它鎖。在所有隔離級別級別中的修改操作(insert、update、delete)都會添加排他鎖,而讀操作可以通過在語句后面添加 for update 來對讀取的數據添加排它鎖。
其他種類
1、Record Lock
記錄鎖。record lock 是加在具體記錄對應聚簇索引上的鎖,它是鎖住的是索引本身而不是記錄,如果該表沒有聚簇索引,也會創建一個聚簇索引來代替。換句話說 record lock 屬於行級鎖。它既可以是共享鎖也可以是排它鎖(究竟是共享鎖還是排他鎖上面已經分析了)。任何級別都會存在。
2、Gap Lock
間隙鎖,就是加在兩條數據索引之間的鎖,比如數據表student(id,name),id 是主鍵,有數據(5,"aa"),(7,"bb"),隔離級別是可串行化。此時事務1執行select * from student where id>5 and id<7,那么就會對 (4,7) 添加間隙鎖,鎖住中間的間隙。比如說事務2執行insert into(6,"cc"),那么次操作就會被阻塞。在可重復讀及以上級別才會有。間隙鎖是一種共享鎖,多個事務可以對同一個間隙添加間隙鎖。
3、Next-Key Lock
指的是 Record Lock 與 Gap Lock 的結合。針對 Gap Lock 中的例子,如果事務1執行的是 select * from dept where id>4 and id<8,那么對數據(5,"aa")、(7,"bb")對應的聚簇索引上也會添加 Record Lock。同時(4,5),(5,7),(7,8)也會加上間隙鎖。同 Gap Lock 一樣,只有可重復讀以以上級別才會出現。next-key lock 都是 “左開右閉” 的,也就是以前面的事務1來舉例,會添加的鎖依次是:net-key lock (4,5]、(5,6]、(6,7],間隙鎖(7,8)。
4、MDL(MetaData Lock)
后面補充的 Lock,其本質屬於表級鎖,在數據增刪改和表結構變化時自動進行加鎖、解鎖,增刪改操作會加讀鎖,修改表結構會加寫鎖。詳情可見 Mysql 中的MDL鎖。
5、自增鎖
詳情可見 MySQL中的自增主鍵。
四種隔離級別的實現
在說明原理前,先了解一下什么是快照讀和當前讀。
快照讀:Mysql 默認的隔離級別是“可重復讀”。通過文章開頭的例子可以看出左邊事務在右邊事務執行修改提交前后查詢的數據都一樣,左邊事務的查詢就是一個快照讀。快照讀的數據可以看作一個快照,其他事務的修改不會改變這個快照值。也就是說快照讀的數據不一定是最新值,可重復讀級別也因此才保證了 “可重復讀”。快照讀的優勢是不用加鎖,並發效率高。
使用場景:在 Mysql 的隔離級別中,除了可串行化級別的讀外,其他隔離級別中事務的讀都是快照讀。
當前讀:當前讀指的就是讀的是最新值。既然是要求是最新值,那么就需要進行加鎖限制,所以當前讀是需要加鎖的,同時因為當前讀一定是最新的數據,所以就無法保證 “可重復讀”。
使用場景:首先是可串行化中事務的讀操作是當前讀,而四種隔離級別中的所有修改(insert、update、delete)操作都屬於當前讀。可能你覺得讀操作和修改操作沒有關系,但是事實是這些修改操作是先 “讀” 找到數據具體的位置才能進行 “修改”。
讀已提交和可重復讀的實現
這兩種隔離級別的實現歸功於 MVCC 機制。
MVCC機制
MVCC機制也叫多版本並發控制,用於控制數據庫的並發訪問。在 Mysql 的 InnoDB 存儲引擎中主要作用於實現讀已提交和可重復讀隔離級別。實現原理是通過 undo日志版本鏈和 Read View 。
1、undo日志版本鏈。在 InnoDB 聚簇索引記錄的行數據中有兩個隱藏列,trx_id 和 roll_pointer,trx_id 表示當前行數據上次被修改的事務 id (事務 ID 是自增的,越新的事務 ID 越大),roll_pointer 是每次在修改完數據前,都會將修改前的數據存入undo log(專門用於記錄事務修改前數據的日志系統,用於進行事務的回滾和生成數據快照),roll_pointer 就是當前行數據修改前在 undo 日志中的存儲位置。
2、Read View。內部主要有四個部分組成,第一個是創建當前 Read View 的事務 id creator_trx_id,第二個是創建 Read View 時還未提交的事務 id 集合trx_ids,第三個是未提交事務 id 集合中的最大值up_limit_id,第四個是未提交事務 id 集合中的最小值low_limit_id。
當執行查詢操作時會先找磁盤上的數據,然后根據 Read View 里的各個值進行判斷,
1)如果該數據的 trx_id 等於 creator_trx_id,那么就說明這條數據是創建 Read View的事務修改的,那么就直接返回;
2)如果 trx_id 大於等於 up_limit_id,說明是新事務修改的,那么會根據 roll_pointer 找到上一個版本的數據重新比較;
3)如果 trx_id 小於 low_limit_id,那么說明是之前的事務修改的數據,那么就直接返回;
4)如果 trx_id 是在 low_limit_id 與 up_limit_id 中間,那么需要去 trx_ids 中對各個元素逐個判斷,如果存在相同值的元素,就根據 roll_pointer 找到上一個版本的數據,然后再重復判斷;如果不存在就說明該數據是創建當前 Read View 時就已經修改好的了,可以返回。
而讀已提交和可重復讀之所以不同就是它們 Read View 生成機制不同,讀已提交是每次 select 都會重新生成一次,而可重復讀是一次事務只會創建一次且在第一次查詢時創建 Read View。事務啟動命令begin/start transaction不會創建Read View,但是通過 start transaction with consistent snapshot 開啟事務就會在開始時就創建一次 Read View。
舉個網上的例子,啟動事務的方式是通過 start transaction with consistent 。首先創建事務1,假設此時事務1 id 是60,事務1先修改 name 為小明1,那么就會在修改前將之前的記錄寫入 undo log,同時在修改時將生成的undo log 行數據地址寫入 roll_pointer,然后暫不提交事務1。開一個事務2,事務 id 為 65,進行查詢操作,此時生成的 Read View 的trx_ids是[60],creator_trx_id 為 65,對應的數據狀態就是下圖,首先先得到磁盤數據的 trx_id ,為60,然后判斷,不等於 creator_trx_id,然后檢查,最大值和最小值都是 60,也就是屬於上面 2)的情況,所以通過 roll_pointer 從 undo log 中找到 “小明” 那條數據,再次判斷,發現 50 是小於 60的,滿足上面 3)的情況,所以返回數據。
然后提交事務1,再開一個事務3,將name改成小明2,假設此時的事務3 id 是100,那么在修改前又會將 trx_id 為 60 拷貝進 undo log,同時修改時將 trx_id 改為100,然后事務3暫不提交,此時事務1再進行select。如果隔離級別是讀已提交,那么就會重新生成 Read View,trx_ids是[100],creator_trx_id 為65,判斷過程和上面相似,最終返回的是小明1那條數據;而如果是可重復讀,那么還是一開始的 Read View,trx_ids 還是[60],creator_trx_id 還是 65,那么還是從小明2 的 trx_id 進行判斷,發現不等於 65,且大於60,為情況 2),跳到 小明1 ,對 trx_id判斷,還是大於,還是情況 2),跳轉到 “小明” 那條數據,判斷 trx_id < low_mimit_id,為情況 3),所以返回 "小明"。下面是這個例子最終的示意圖
讀未提交和可串行化實現
這兩個實現比較簡單。讀未提交就是每次事務執行的修改都更新到對應的數據上,然后讀取直接讀取這個數據就可以了。而可串行化則是使用了讀鎖和寫鎖以及間隙鎖來實現的,對會造成“幻讀”、“臟讀”、“不可重復讀” 的操作會進行阻塞,也正因為這樣,極易任意造成阻塞,所以不建議使用可串行化級別。
不同隔離級別下加鎖情況
鎖是加載索引上的。對於不同的隔離級別,不同的列情況,加鎖情況都各不不同,下面會列舉各個場景下加鎖的情況。
1、讀未提交級別
讀操作不會加鎖,寫操作會添加排它鎖。因為會發生臟讀,所以 MVCC並不會發生效果。可以手動添加 for update 、lock in share mode 來加鎖,不會產生間隙鎖,只有記錄鎖。
無論是否使用索引,是否是手動添加鎖,只會對最終操作的數據加 Record Lock。
2、讀已提交級別
讀操作不會加鎖,寫操作會添加排它鎖。MVCC 會在每次查詢時生成 Read View,可以手動添加 for update 、lock in share mode 來加鎖,不會產生間隙鎖,只有記錄鎖。
無論是否使用索引,是否是手動添加鎖,只會對最終操作的數據加 Record Lock。(在未使用到索引時數據庫會對所有數據加鎖,當加載到 Server 層篩選后會將不符合條件的數據進行解鎖,所以我們會認為只對最終操作的數據加鎖,讀未提交級別的未使用索引情況也相同)
3、可重復讀級別
可重復讀是一個特殊的隔離級別,為什么這么說呢?因為它是 mysql 默認的隔離級別,因為 "可串行化" 級別默認對讀操作加鎖,導致程序的並發性不高,所以不建議使用,而可重復讀因為使用的是快照讀,所以並發性很好,並且解決了不可重復讀、臟讀以及 "快照讀" 幻讀,但同時會有 "當前讀"幻讀的問題產生(下面"MySQL 對幻讀的解決" 會詳細解釋),所以針對這個問題引入了間隙鎖來解決。
讀操作不會加鎖,寫操作會添加排它鎖。MVCC 會在事務開始第一次查詢時生成 Read View,可以使用 for update、lock in share mode 來手動加鎖,可能會產生間隙鎖。
在默認加鎖情況下,加鎖的規律可以概括為下面幾點:
原則1:加鎖的基本單位是 next-key lock。需要注意的是,next-key lock 都是前開后閉區間。
原則2:查找過程中訪問到的對象才會加鎖。
原則3:非唯一索引會向排序方向一直匹配,直到不滿足條件為止。唯一索引不需要。
優化1:索引上的等值查詢(符號是=。或者是首個判斷條件的常量值等於第一個確定的值,比如有唯一索引列 a 值5、10,條件為 a>=5(a>5 不行,必須包含等於的符號),那么根據條件找到第一個滿足的記錄值5正好等於條件的邊界值 5,那么也可以優化成行鎖),給唯一索引加鎖的時候,next-key lock 會退化成行鎖。
優化2:索引上的等值查詢(符號必須是=),向右遍歷時且最后一個值不滿足等值條件的時候,next-key lock 會退化成間隙鎖。
一個 bug:唯一索引上的范圍查詢會訪問到不滿足條件的第一個值為止。
除了上面五種情況外,還有一些場景需要注意:1、因為默認的排序方式是升序,如果聲明為 desc 那么就需要從高向低匹配。 2、上面說的都是等值,如果是不等值,如 >、<、>=、<=,那么會先匹配到最先滿足條件的值(其實就是找等值沒有找到所以按着結點值向條件方向找到一個返回),然后根據上面的加鎖規則加鎖。 3、使用 limit x 會在加鎖的元素達到 x 后就會停止加鎖。
下面就舉幾個例子來說明。首先是表結構和數據。
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、等值查詢未命中。
sessionA 首先根據原則1,添加 next-key lock ( 5, 10 ],然后因為優化2, 10 不滿足等值條件,降為間隙鎖,所以最終加鎖為間隙鎖 (5,10)。
2、倒序唯一索引查詢加鎖情況。執行語句
begin; select * from t where id>9 and id<12 order by id desc for update;
這條語句加鎖情況是 next-key lock(0,5 ]、( 5,10 ],間隙鎖(10,15 )。加鎖過程:因為是按 id 降序,所以先根據 id<12 來查找第一個符合條件的值,也就是 c=10,然后根據原則1直接添加 next-key lock(5,10 ],同時因為還未達到 id<12 的條件,所以還需要先添加next-key lock(10,15 ],但是因為優化2,所以這個 next-key lock 會退化成間隙鎖,所以變成(10,15)。然后開始向前匹配,因為前置條件是 id>9 ,所以在找到 id=5這條記錄后才確定遍歷完所有滿足的記錄,這時因為原則2,所以需要對 c=5 這行添加next-key lock(0,5](這里的條件不是等值查詢條件(必須為=),所以優化1,2不會生效)。
3、非唯一索引發生覆蓋索引不需要加鎖。執行
sessionA 加鎖情況是間隙鎖(0,5)、(5,10)。加鎖過程:首先會匹配 c=5 這行,因為原則1,所以加 next-key lock(0,5 ]。又因為 c 不是唯一索引,必須繼續向右匹配,直到不符合條件的值結束(這里不向前匹配是因為在初始確定第一個值時就一定是邊緣值,所以在 c=5 前面的記錄一定不是 c=5,不需要匹配)。向右匹配找到 c=10 后發現不滿足,結束。因為原則2,所以會添加一個 next-key lock(5,10 ]。但是因為優化2,c=10不滿足條件,所以退化成間隙鎖(5,10)。同時由於 sessionA 是覆蓋索引,並沒有訪問主鍵索引,所以並不需要加鎖(因為鎖是加載聚簇索引上的)。最終加鎖情況就是兩個間隙鎖(0,5)、(5,10)。
4、唯一索引范圍查詢。執行
sessionA 加鎖情況是[ 10,15 ]。加鎖過程:首先根據 id>=10 找到 id=10 這一行,因為原則1加鎖 next-key lock(5,10 ],但是因為優化1,所以這里的 next-key lock 被優化成行鎖 c=10。然后向右遍歷,遍歷到 id=15 后發現不滿足條件所以加鎖 next-key lock(10,15 ](條件不是=,所以優化1,2不生效)。所以最終的加鎖區間就是 [10,15 ]。
5、唯一索引 bug。該 bug 在 8.0.18 版本中修復。執行
如果不看 bug,只根據前面的加鎖規律來看:首先根據條件 id>10 ,找到滿足的第一個值,也就是 id=15,這時因為原則1,添加 next-key lock(10,15 ],然后向右遍歷,找到 id=15 后就已經滿足了后置條件,所以最終的鎖就是(10,15 ]。但是實際並不是如此。sessionB、sessionC都被鎖住了。
實際的加鎖:在添加完 next-key lock(10,15 ] 后,還會匹配直到不滿足條件為止,也就是將非唯一索引的加鎖規則弄到了唯一索引上。所以在遍歷到 id=20 后才停止,所以相對應的又添加了一個 mext-key lock(15,20 ]。
6、使用 limit x 會在加鎖的元素達到 x 后就會停止加鎖。加入增加了一行記錄 (30,10,10),執行 SQL :delete from t where c=10 limit 2; c是索引列,那么其加鎖情況如下:
加鎖過程:首先匹配到(10,10,10)這一行記錄,然后添加 next-key lock((5,5),(10,10) ],這是第一個值,然后向右匹配,找到 (30,10,10),添加 next-key lock((10,10),(10,30) ]。到這兩個值都已經匹配完成,所以即使是非唯一索引,也不會再向右邊匹配。
7、使用 in 就相當於三條等值查詢。執行SQL:select id from t where c in(5,20,10) lock in share mode;
加鎖情況:因為是默認升序的,所以執行是先執行 c=5,然后 c=10,最后 c=20。c=5加鎖是(0,5 ]、(5,10)(因為優化2);c=10加鎖是(5,10 ]、(10,15);c=20 加鎖是(15,20 ]、(20,25)。
8、特殊情況1。刪除操作可能會使間隙鎖范圍擴散。
sessionA 原本添加的鎖是一個 next-key lock(10,15 ]。所以 sessionB 刪除是不會被阻塞的,但是在刪除后因為間隙失去了,就會轉移到前一個點上,也就是間隙擴大,所以鎖就變成了(5,15 ]。
9、特殊情況2。更新操作也可能會使間隙鎖范圍擴散。
更新操作可以看成兩步:先新增修改后的記錄,然后再刪除要修改的記錄。
分析:sessionA 加鎖區間是(5,+∞)所以這兩步過程如下:
第一步:先增加c=1的記錄,刪除c=5的記錄,鎖區間因為刪除擴大變成(1,+∞);
第二步:增加 c=5的記錄,被鎖阻塞。
10、空表的加鎖情況是 next-key lock (-∞, supremum]。
4、可串行化級別
讀操作會加讀鎖,寫操作會加寫鎖,讀寫鎖互斥。也會有間隙鎖。
1)用到主鍵索引和唯一索引,會對操作數據添加 Record Lock。
2)普通索引,會對操作數據以及間隙添加 Next-key Lock。
3)未使用索引,會對所有數據以及兩邊間隙添加 Next-key Lock。
MySQL 對幻讀的解決
“快照讀” 幻讀
通過上面對 MVCC 原理的解釋,可以知道文章開頭的例子為什么“解決了” 幻讀。如果假設左邊的事務1 id 是50,右邊事務2 id 是55,其他數據創建時的事務是10,那么在事務1第一次查詢時生成的 Read View 的 trx_ids 為[55],對應的數據如下
那么在判斷其他數據時 trx_id 的10小於 trx_ids 的最小值55,所以通過,而 id 為6的數據發現 trx_id 正好等於 55,所以獲取 roll_point 從 undo log中找到之前的數據快照,但是發現該列值為空,所以放棄跳到下一條數據。沒有出現文章開頭所說的 “幻讀” 情況,開頭所說的幻讀叫做 “快照讀” 幻讀。 由此我們可以知道, MVCC 可以解決 “快照度” 幻讀。
這里可以再插入一下題外話,其實對於 MVCC 中可重復讀級別 Read View 創建時機為什么是第一次查詢時生成而不是事務啟動時就生成,可以通過下面的測試來證明。
可以在事務2提交后再查詢就會查出提交后的數據。
“當前讀” 幻讀
這樣看來 MVCC 已經解決了幻讀問題,而在一開始也說過在默認時在一定程度上解決了幻讀,為什么這么說?請看下面這個例子
如果單看左邊的事務,會發現明明表中沒有id為6的記錄,但是就是無法執行 insert 操作,顯示主鍵已存在。這就是 “當前讀”幻讀,而 MVCC只能解決 “快照讀” 幻讀。由於前面對 “當前讀”、“快照讀” 的解釋可以知道這兩種讀是互斥的,那么如何解決 “當前讀” 幻讀。第一種方式是直接切換成 “可串行化” 級別,這種因為默認對數據加鎖,不利於項目的並發執行,所以不建議;第二種就是手動添加鎖,在開始查詢操作后添加 for update 或 lock in share mode,來強制加鎖(也對間隙加上了間隙鎖),這樣右邊的插入操作就會被阻塞。這樣就可以實現 "當前讀"了。
死鎖
MySQL 的死鎖與多線程中的死鎖本質上一樣,其核心思想就是 “兩個及以上的事務互相獲取對方事務添加的鎖記錄(排它鎖)”,
解決死鎖的方式有兩種:
1、設置事務超時時間。通過修改 innodb_lock_wait_timeout 參數來修改事務的超時時間,默認為 50s。這種方式不推薦使用,因為並不能根本上解決問題,縮短時間雖然能解決死鎖,但是對於執行時間長的事務來說就永遠無法完成。
2、開啟死鎖檢測,在事務獲取鎖資源時阻塞就會開始檢測,如果發生死鎖就會回滾其中一個事務。這是默認的方式,死鎖檢測可以通過將 innodb_deadlock_detect 設為 on 來開啟(默認開啟)。但是死鎖檢測會消耗大量的 CPU。所以可以進行一些優化。
優化方式:
1、在保證當前系統不會發生死鎖后可以直接關閉搜索檢測。
2、控制並發度,通過使用中間件來減少數據庫操作的並發量來提高效率。
3、將操作的數據拆分成多個數據,然后依次獲取,這樣就減小了資源沖突的概率,但是需要結合業務判斷是否允許拆分數據。
Next-key Lock 是 Record Lock 與 Gap Lock 結合的驗證
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);
可重復讀隔離級別下
Gap Lock 是共享鎖,但是行鎖、Next-key Lock不是,如果這樣的話因為 sessionA 開始加的鎖就是 Next-key Lock( (5,5,5) , (10,10,10) ],間隙鎖 ( (10,10,10) , (15,15,15) )。所以 sessionB 應該是無法獲取 Next-key Lock( (5,5,5) , (10,10,10) ],所以 sessionA 的 insert 語句就不應該被阻塞,也就不會造成死鎖,但為什么會這樣呢?這是因為 Next-key Lock 並不是一把原子性的鎖,它是由 Gap Lock 與 Record Lock 組成的,在 sessionB 的 update 語句執行時首先添加的是間隙鎖( (5,5,5) , (10,10,10) ),然后再嘗試添加(10,10,10) 的行鎖,然后失敗,隨后后面的間隙鎖也會被阻塞加鎖。所以最終加鎖范圍就是 ( (5,5,5) , (10,10,10) )。所以 sessionA 在執行 insert 時也會等待 sessionB 釋放間隙鎖后才能執行,導致死鎖。
而如果把 sessionA 的insert 操作改成 update t set d=d+1 where c=10; 會發現還是阻塞發生死鎖,這是因為 sessionA 開始加的是共享鎖,只能讀不能寫,如果改成 select * from t where c=10 for update 就不會發生死鎖了。
博客總結來源於
https://blog.csdn.net/cug_jiang126com/article/details/50596729
https://www.cnblogs.com/crazylqy/p/7611069.html,其中一些圖片和加鎖情況來源於第二個
以及極客時間的<<mysql實戰45講>>