前言
在學習seata時,解決懸掛問題,使用select for update語句進行當前讀,插入事務狀態語句過程中,出現死鎖異常:
Deadlock found when trying to get lock; try restarting transaction
於是出現如下疑問:
insert語句爲什麼會出現死鎖?
本片文章主要分析:
- 鎖的基本概念
- 常見語句加鎖類型
- 兩種死鎖產生原因(間隙鎖插入意向鎖導致死鎖、主鍵衝突導致死鎖)
1.鎖的基本概念
本次鎖分析都是基於innodb數據存儲引擎。
1.1 行級鎖
共享鎖:S鎖,允許事務讀一行數據。SELECT LOCK IN SHARE MODE
排他鎖:X鎖,允許事務刪除或更新一行數據。SELECT FOR UPDATE | UPDATE | DELETE
||X|S| |-|-|-| |X|不兼容|不兼容| |S|不兼容|兼容| X鎖與任何的鎖都不兼容,而S鎖僅和S鎖兼容。
注意:行鎖實際上是索引記錄鎖,對索引記錄的鎖定。即使表沒有建立索引,InnoDB也會創建一個隱藏的聚簇索引,並使用此索引進行記錄鎖定。
1.2 意向鎖
意向鎖定是表級鎖定,標識事務稍後對錶中的行做哪種類型的鎖定(共享或獨佔)
意向共享鎖(IS):事務想要獲得一張表中某幾行的共享鎖
意向排他鎖(IX):事務想要獲得一張表中某幾行的排他鎖
意圖鎖遵循如下協議:
在事務獲取表中某行的共享鎖之前,它必須首先在表上獲取IS鎖或更強的鎖。
在事務獲取表中某行的獨佔鎖之前,它必須首先在表上獲取IX鎖。
注意:意向鎖只會阻塞表級別的鎖(如LOCK TABLES請求的表鎖),並不會阻塞行級鎖(如行級X鎖)。
1.3 行鎖分類
鎖名稱 | 描述 |
---|---|
Record Lock | 單行記錄鎖 |
Gap Lock | 間隙鎖,鎖定一個範圍,但不包含記錄本身 |
Next-Key Lock | 鎖定單行記錄以及記錄前一個間隙 |
1.4 插入意向鎖(Insert Intention Lock)
插入意圖鎖是在行插入之前通過INSERT操作設置的一種特殊間隙鎖。
注意:多個事務插入同一個間隙的不同位置,他們並不會衝突。 假設存在索引記錄,其值分別爲4和7。單獨的事務分別嘗試插入值5和6,在獲得插入行的排他鎖之前,每個事務都使用插入意圖鎖來鎖定4和7之間的間隙, 但他們不會互相阻塞。
同樣,不同事務請求同一個間隙的Gap鎖並不會阻塞,但如果一個事務請求了Gap鎖,另一個事務再請求插入意向鎖,則會阻塞。
1.5 間隙鎖意義
mysql查詢分爲快照讀和當前讀。
快照讀(select)通過mvcc解決幻讀問題。下圖事務1在事務2插入語句前,進行了一次快照讀,事務2插入語句提交後,事務1再次進行快照讀,同樣不能查詢到事務2剛插入的記錄。
事務1執行當前讀,就可以查詢到事務2 剛插入的語句。
當前讀(select for update)通過間隙鎖解決幻讀問題。間隙鎖通過阻塞插入意向鎖解決幻讀。
在上面例子的基礎上(事務1已經進行當前讀,還未完成事務),我們開啓事務3,執行插入語句,事務3被事務1的當前讀阻塞。
2.常見語句加鎖類型
2.1 insert
- 加上表級意向排他鎖。
- 檢測主鍵衝突,如果存在衝突,則獲取共享鎖S進行當前讀。
- 衝突檢測通過,判斷插入數據位置是否有間隙鎖,有就等待間隙鎖釋放。
- 無間隙鎖,獲取插入意向鎖,插入數據。
2.2 update
- 如果修改數據在表裏存在,並且where語句存在唯一索引,加行級記錄鎖,鎖定一行。
- 如果修改數據在表裏存在,並且where語句不存在唯一索引,對記錄加鎖,並且對where語句前後的間隙枷鎖。
- 如果修改數據在表裏存在,並且where語句沒有索引,全表加鎖。
- 如果數據在表裏不存在,where語句存在索引,對命中間隙加鎖。
- 如果數據在表裏不存在,where語句沒有索引,全表加鎖。
2.3 delete
和update一樣,取決於where語句。
2.4 select
和update一樣,取決於where語句。
3.死鎖
mysql8可以通過以下語句查詢鎖狀態:
select ENGINE_TRANSACTION_ID,OBJECT_NAME,INDEX_NAME,LOCK_TYPE,LOCK_MODE,LOCK_STATUS,LOCK_DATA from performance_schema.data_locks;
3.1 間隙鎖和插入意向鎖造成的死鎖
3.1.1 最終目的
事務1開啓當前讀,查詢age=26的數據是否存在,不存在就插入。
事務2開啓當前讀,查詢age=27的數據是否存在,不存在就插入。
3.1.1 開啓事務1和事務2
創表語句:
CREATE TABLE `test1`.`user` (
`id` bigint(0) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_name` varchar(16) NOT NULL,
`age` int(0) NOT NULL,
PRIMARY KEY (`id`),
INDEX `idx_user_age`(`age`) USING BTREE
);
user表初始數據如下:
3.1.2 事務1和事務2都執行快照讀
事務1執行:
select id, user_name, age from user where age = 26 for update;
事務2執行:
select id, user_name, age from user where age = 27 for update;
來看下兩個事務都執行快照讀後的鎖狀態:
事務1(事務編號1863)先獲取到表級意向排他鎖IX,然後對age=26的數據進行當前讀,因爲表中沒有age=26的數據,所以對age>25之後的數據都加上間隙鎖。事務2同理。
3.1.3 事務1執行插入
事務1執行:
insert into user(user_name, age) values('lisi', 26);
因爲事務2已經獲得age>25的間隙鎖,事務1在執行insert時,只能等待事務2釋放間隙鎖。
可以看到,在對age索引加鎖的同時,也對主鍵索引進行了加鎖,防止主鍵衝突。
3.1.4 事務2執行插入
事務2執行:
insert into user(user_name, age) values('wangwu', 27);
事務2在執行insert語句時,需要等待事務1釋放age>25的間隙鎖,導致事務2等待。
這時mysql檢測到死鎖出現,立即回滾事務2。事務1獲取到age>25的間隙鎖,執行完成insert。
事務2回滾後事務狀態如下:
3.1.5 死鎖日誌
通過以下語句查詢mysql最近一次死鎖日誌:
show engine innodb status;
是否有事務1和事務2都能同時獲得排他鎖的疑問?
因爲InnoDB中的間隙鎖的唯一目的是防止其他事務插入間隙。間隙鎖是可以共存的,一個事務佔用的間隙鎖不會阻止另一個事務獲取同一個間隙上的間隙鎖。
3.2 主鍵衝突導致死鎖
3.2.1 最終目的
事務1插入id=5的數據。
事務2插入id=5的數據。
3.2.2 準備階段
開始3個事務。
3.2.3 事務3執行insert id=5
事務3獲取到插入意向鎖,完成插入,事務3還未提交。
3.2.4 事務1和事務2執行insert id=5
事務3獲得id=5的行級排他鎖(REC_NOT_GAP表示非間隙鎖),
事務1和事務2此時由於主鍵衝突,需要先拿到id=5的共享鎖S執行當前讀,此時事務3獲得id=5的排他鎖X,事務1和事務2不能獲取到共享鎖S,只能等待事務3釋放排他鎖X。
3.2.5 回滾事務3 出現死鎖
由於事務3回滾id=5的排他鎖X,事務1和事務2都拿到id=5的共享鎖S查詢是否存在id=5的數據。
事務3回滾,id=5的數據不存在,事務1和事務2都準備獲取查詢意向鎖插入id=5的數據,由於插入意向鎖是排他鎖,和共享鎖互斥,事務1只能等待事務2釋放共享鎖S,事務2只能等待事務1釋放共享鎖S,產生死鎖。