MySQL鎖(讀鎖、共享鎖、寫鎖、S鎖、排它鎖、獨佔鎖、X鎖、表鎖、意向鎖、自增鎖、MDL鎖、RL鎖、GL鎖、NKL鎖、插入意向鎖、間隙鎖、頁鎖、悲觀鎖、樂觀鎖、隱式鎖、顯示鎖、全域性鎖、死鎖)

小松聊PHP进阶發表於2024-03-10

本文說明的是MySQL鎖,和作業系統或者程式語言的鎖無關。

概念

作用:在併發情況下讓資料正確的讀寫。
優點:併發情況下對資料讀寫可控,防止出錯。
缺點:降低效能、增加難度。

分類

  • 資料操作型別劃分
    • 讀鎖(共享鎖、S鎖)
    • 寫鎖(排它鎖、獨佔鎖、X鎖)
  • 粒度劃分
    • 表級鎖
      • S鎖、X鎖
      • 意向鎖
      • 自增鎖
      • 後設資料鎖
    • 行級鎖
      • 記錄鎖
      • 間隙鎖
      • 臨鍵鎖
      • 插入意向鎖
    • 頁級鎖
  • 嚴格度劃分
    • 悲觀鎖
    • 樂觀鎖
  • 加鎖方式
    • 隱式鎖
    • 顯示鎖
  • 其它
    • 全域性鎖
    • 死鎖

測試用表

CREATE TABLE `cs` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `num1` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '數字列1',
  `num2` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '數字列2',
  `s1` varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字串列1',
  `s2` varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字串列2',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `temp`.`cs` (`id`, `num1`, `num2`, `s1`, `s2`) VALUES (1, 1, 1, 'abc', 'xyz');
INSERT INTO `temp`.`cs` (`id`, `num1`, `num2`, `s1`, `s2`) VALUES (2, 2, 2, 'ABC', 'XYZ');

讀鎖、共享鎖、S鎖

讀鎖、共享鎖、S鎖(Share Lock)是一個東西。
鎖的是:允許同時有多個事務,對資料只能讀,不能寫。
寫法:

select ... lock in share mode;
select ... for share; #MySQL8

測試:

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select * from cs where id = 1 lock in share mode; select * from cs where id = 1 lock in share mode; 雙方新增共享鎖,都能成功新增
3 commit; commit; 正常提交事務,無報錯

寫鎖、排它鎖、X鎖、獨佔鎖

寫鎖、排它鎖、X鎖(Exclusive Lock)、獨佔鎖是同一個東西。
鎖的是:僅允許同時有1個鎖獨佔該事務,具有排它性,不允許其它任何型別的鎖再佔用該事務。

select ... for update;

測試:
X鎖排斥S鎖1

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select * from cs where id = 1 lock in share mode; select * from cs where id = 1 for update; 會話A新增S鎖,會話B新增X鎖
3 / 阻塞 會話B新增X鎖被阻塞,說明X鎖有排它性
4 commit; / 會話B X鎖新增成功
5 / commit; 會話B提交事務,結束流程

X鎖排斥S鎖2

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select * from cs where id = 1 for update; select * from cs where id = 1 lock in share mode; 會話A新增X鎖,會話B新增S鎖
3 / 阻塞 會話B新增S鎖被阻塞,說明X鎖有排它性
4 commit; / 會話B S鎖新增成功
5 / commit; 會話B提交事務,結束流程

X鎖排斥X鎖

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select * from cs where id = 1 for update; select * from cs where id = 1 for update; 雙方新增X鎖
3 / 阻塞 會話B新增X鎖阻塞,證明排它性
4 commit; / 會話B X鎖新增成功
5 / commit; 會話B提交事務,結束流程

S鎖、X鎖常見誤區

注意:在同一個事務裡面,行級X鎖或S鎖,允許其它SQL語句對此行的寫操作。
如下,每條SQL都能成功執行。
所以說鎖,鎖的是對外的事務,對內(當前會話)不做限制。

start transaction;
select * from cs where id = 1 for update;
update cs set num1 = 2 where id = 1;
commit;

start transaction;
select * from cs where id = 1 lock in share mode;
update cs set num1 = 2 where id = 1;
commit;

注意:在任意個事務裡面,可以對同一條不存在的資料共同新增X和S鎖,不會阻塞。

事務A
start transaction;
select * from cs where id = 1234 for update;
select * from cs where id = 1234 lock in share mode;

事務B
start transaction;
select * from cs where id = 1234 for update;
select * from cs where id = 1234 lock in share mode;

事務A
commit;

事務B
commit;

注意:事務A加了X鎖,不影響事務B讀這條資料。

事務A
start transaction;
select * from cs where id = 1 for update; #能正常讀出資料

事務B
start transaction;
select * from cs where id = 1;

事務A
commit;

事務B
commit;

對阻塞超時時間的最佳化

遇到X鎖會阻塞,預設阻塞50秒,50秒過後阻塞停止,但不會自動的回滾或者提交事務。MySQL提供了一些阻塞時間的最佳化方向
方式1:配置最佳化

檢視阻塞時間,單位秒
show variables like 'innodb_lock_wait_timeout';
或
select @@innodb_lock_wait_timeout;

修改
set session innodb_lock_wait_timeout = 10; #當前會話生效
或
set global innodb_lock_wait_timeout = 10; #全域性生效,重啟後新配置丟失
注意global的修改對當前執行緒是不生效的,只有建立新的連線才生效

或者修改配置檔案
vim /etc/my.cnf
[mysqld]
innodb_lock_wait_timeout = 10

方式2:SQL語句層面,MySQL8新特性
select ... nowait:會立即停止,但報錯。
select ...skip locked:會立即停止,但不會報錯,結果中不包含被鎖定的行。

表鎖、頁鎖、行鎖

這是鎖定的粒度,3個依次遞減,粒度越小越好。力度小意味著更少的資源被鎖定,可以提高併發性。

表鎖

表鎖就是鎖住了整張表,所以表鎖比行鎖,更不容易發生死鎖的情況。
注意:MyISAM引擎使用select,會給當前表加上S鎖,在寫操作時會加上X鎖。

檢視一些資料庫的表,都加了那些鎖
show open tables;
In_use: 是否正在被使用,如果為 1 則表示該表正在被某個執行緒使用。
Name_locked: 是否被上了表級鎖,如果為 1 則表示該表被鎖定。
+--------------------+------------------------------------------------------+--------+-------------+
| Database           | Table                                                | In_use | Name_locked |
+--------------------+------------------------------------------------------+--------+-------------+
| performance_schema | events_waits_summary_by_user_by_event_name           |      0 |           0 |
| performance_schema | events_waits_summary_global_by_event_name            |      0 |           0 |
+--------------------+------------------------------------------------------+--------+-------------+


為某個表新增X鎖
lock table 表名 write;

為某個表新增S鎖
lock table 表名 read;

取消表鎖,部分XS
unlock tables;

表鎖許可權如下(MySQL5.7)

表鎖型別 當前會話對當前表可讀 當前會話對當前表可寫 其它會話對當前表可讀 其它會話對當前表可寫
S鎖 阻塞
X鎖 阻塞 阻塞 阻塞

表級鎖->意向鎖、意向共享鎖、意向排它鎖、IS鎖、IX鎖

意向鎖(Intention Shared Lock)是自動維護的鎖,用於表級別上表明事務對錶中某個行的操作意向,意思是在表級別上宣告瞭某個行已經加鎖,最佳化事務之間的併發訪問。意向鎖分為兩種:意向共享鎖(IS)和意向排它鎖(IX)。
MyISAM引擎不支援,InnoDB引擎支援。
MySQL新增行級鎖,都會向上宣告新增了意向鎖。
注意意向排它鎖與意向排它鎖不衝突,意向排它鎖與意向共享鎖都不衝突,如果衝突了,鎖的靈活性將大大降低。距離說明:兩個不同的行X鎖各生成一個IX鎖,如果IX鎖衝突,則無法建立兩個行X鎖,這種巨大的bug不允許出現。

優點:

  • 提高併發性:如果沒有意向鎖,在一個百萬級的大表中加一個鎖,可能需要逐行遍歷,看看有沒有加其它鎖,會影響當前新增的鎖,但是有了意向鎖,在表的層面就可以獲取先前的鎖,提高效能。
  • 避免死鎖:透過意向鎖,可以快速判斷哪些鎖請求是互斥的,減少鎖任務堆積引起的複雜度增加,複雜度一上來,就容易有死鎖。

演示:行X鎖與表S鎖

步驟 會話A 會話B 說明
1 start transaction; / 會話A開啟事務
2 select * from cs where id = 1 for update; / 會話A加一個行X鎖,相當於自動新增了一個IX鎖
3 / lock table cs read; 會話B新增表S鎖
4 / 阻塞 這個過程就是表鎖檢測到了事先新增的IX鎖,行級X鎖排斥表級S鎖,阻塞了
5 commit; 成功加上了表級S鎖 會話A事務提交,鎖資源釋放
6 / unlock tables; 釋放表級S鎖

表級鎖->自增鎖、AI鎖

自增鎖(Auto Increment Lock)是指對自增長列(一般是主鍵)確保唯一性的一種鎖機,如果沒有自增鎖,多個事務併發的執行,該列的值就可能一致。
為了避免重複,自增鎖可能會導致插入操作的序列化,降低併發效能,這是它的缺點。
演示:

步驟 會話A 會話B 說明
1 start transaction; start transaction; 雙方開啟事務
2 INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'); INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'),(2, 2, 'ABC', 'XYZ'); 雙方新增3條記錄
3 commit commit; 提交事務
4 select id from cs select id from cs 並未發現報錯,或者id重複的現象

表級鎖->後設資料鎖、MDL鎖

MDL(Metadata Lock)聽起來高大上,實際上就是DDL操作時自動加鎖,所以不需要手動處理。
當要對錶做增刪改查操作時,會獲取一個 MDL 讀鎖來阻止對錶結構的修改。
更改表結構時,會加MDL寫鎖,阻塞其它執行緒的讀寫操作,直到結構變更操作完成。
演示 MDL讀鎖:

步驟 會話A 會話B 說明
1 start transaction; / 會話A開啟事務
2 INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'); alter table cs add index (num1); 會話B新增一個普通索引
3 / 阻塞 會話B被阻塞
4 commit; 索引新增成功 MDL鎖釋放

演示 MDL寫鎖:

步驟 會話A 會話B 會話C 說明
1 start transaction; start transaction; / AB方開啟事務
2 INSERT INTO cs (num1, num2, s1, s2) VALUES (2, 2, 'ABC', 'XYZ'); alter table cs add index (num2); / 會話B新增一個普通索引
3 / 阻塞 / 用會話A的MDL讀鎖去阻塞會話B
4 / / select * from cs 用會話B的MDL寫鎖去阻塞會話C
5 / 阻塞 阻塞 MDL寫鎖阻塞了查詢操作
6 rollback; rollback; 成功查詢出資料 事務回滾,鎖資源釋放。但undo log不記錄DDL語句,所以索引被新增無法回滾。

行鎖->記錄鎖、RL鎖

記錄鎖(Record Lock)就是普通的對某一行上的X或者S鎖。
注意InnoDB引擎下,支援行鎖MyISAM不支援。
測試:

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select * from cs where id = 1 lock in share mode; select * from cs where id = 1 lock in share mode; 雙方新增共享鎖,都能成功新增
3 commit; commit; 正常提交事務,無報錯

行鎖->間隙鎖、GL鎖

間隙鎖(Gap Lock)就是在資料間隙加的鎖,用於防止事務在一個範圍內插入新的符合條件的行,以避免中間插入的幻讀的問題(MySQL預設的RR隔離級別會產生幻讀問題)。
例如一個一個不連續的id列,1,6,10,如果在1~6之間新增一個鎖,就可以1~6防止幻讀的插入操作,10後面間隙鎖防止幻讀就失效了,需要再次宣告新的間隙鎖。1~6之間沒有具體的資料,所以X間隙鎖,或者是S間隙鎖,沒有什麼區別。

MySQL預設的RR級別會產生幻讀,幻讀是指在同一事務中,先後執行相同的查詢範圍,查詢到的數量不一致(mysql的select機制測不出來,兩個事務需要插入相同的id引起的報錯才能測出來),在併發環境中,幻讀通常是因為其他事務在同一範圍內插入新資料導致的。以下的演示,不演示幻讀,只演示間隙鎖阻止幻讀。

測試:

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select * from cs where id = 4 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (3, 1, 1, 'abc', 'xyz'); 會話A新增間隙鎖,會話B插入資料這個區間的資料
3 / 阻塞 可見間隙鎖可以阻止區間幻讀
4 commit; commit; 會話A關閉事務,會話B提交插入資料

當前id最大值為10,測試大於10的幻讀區間

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 sselect * from cs where id = 14 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (10000, 1, 1, 'abc', 'xyz'); 會話A新增間隙鎖,會話B插入id為10000的資料
3 / 阻塞 可見間隙鎖可以防止最大記錄 ~ +∞之間的幻讀
4 commit; commit; 會話A關閉事務,會話B提交插入資料

當前id最大值為100,同樣的方法測試小於100的幻讀區間

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 sselect * from cs where id = 10001 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (9999, 1, 1, 'abc', 'xyz'); 會話A新增間隙鎖,會話B插入id為9999的資料
3 / 成功插入 間隙鎖未能阻塞幻讀插入
4 commit; commit; 會話A關閉事務,會話B提交插入資料

行鎖->臨鍵鎖、NKL鎖

臨鍵鎖(next-key lock)相當於記錄鎖+間隙鎖,用於防止幻讀的鎖。
測試:現表中有id為1,6,10三條資料。

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select * from cs where id between 1 and 6 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (4, 1, 1, 'abc', 'xyz'); 會話A新增臨鍵鎖,會話B插入id為4的資料
3 / 阻塞 臨鍵鎖阻塞幻讀插入
4 commit; commit; 雙方提交事務,結束流程

行鎖->插入意向鎖、IIL鎖

插入意向鎖(Insert Intention Lock)是間隙鎖的一種,由insert行插入前的操作。該鎖表示插入的意圖,即插入同一索引間隙的多個事務如果沒有在間隙內的相同位置插入,則insert不需要相互等待。
通俗講就是如果間隙鎖得到了釋放,則需要insert的多個事務,不會阻塞。
測試:現表中有id為1,6,10三條資料。

步驟 會話A 會話B 會話C 備註
1 start transaction; start transaction; start transaction; 三方開啟事務
2 select * from cs where id = 5 lock in share mode; INSERT INTO cs (id, num1, num2, s1, s2) VALUES (4, 1, 1, 'abc', 'xyz'); INSERT INTO cs (id, num1, num2, s1, s2) VALUES (4, 1, 1, 'abc', 'xyz'); 事務A上間隙鎖,其餘事務插入資料
3 / 阻塞 阻塞 間隙鎖起作用阻塞B、C會話
4 commit; 成功插入 成功插入 插入意向鎖起作用,讓兩個插入的事務不發生阻塞
5 commit; commit; commit; 提交事務,結束流程

頁鎖

頁鎖是一種鎖定級別,用於控制對資料表中的頁(MySQL對錶資料讀寫的基本單位)的訪問。
由於SQL語句層面,DDL、DML、DQL,都是針對庫表或者表資料的讀寫操作,對於頁極少操作,頁鎖粒度較大,其它鎖範圍使用場景夠用,所以瞭解概念就行。

悲觀鎖

之前寫過詳細的文章可進行參考:MySQL樂觀鎖與悲觀鎖

悲觀鎖比較悲觀,假設資料一定會造成衝突,屬於MySQL層面的鎖。透過加鎖阻塞其他事務,悲觀鎖可以保證在任何時刻,只有一個事務能夠修改或訪問共享資源,從而實現了強一致性。這意味著在悲觀鎖機制下,每個事務的讀寫操作都是有序、線性的。
需要事務的參與。

悲觀鎖,最經典的場景就是防超賣,一共10個庫存,由於併發情況下,兩個請求查詢到的庫存都是10,一個請求下單6個,一個請求下單8個,如果不加鎖使其在事務中阻塞(阻塞可以理解為強制讓事務序列執行),那就會有超賣的情況發生,即使是超賣,庫存也不會顯示為負,因為併發情況下兩個請求檢測到的庫存都是10,大於8和6,最終扣庫存的結果不是10-8-6=-4(這是序列請求的理論值),而是2或者4。

適用場景
寫多寫操作的前提,是保證資料不出錯,悲觀鎖的機制很符合。

優點

  • 強一致性:基於事務又加鎖,一致性可以保證。
  • 實現簡單:在事務中for update即可,開發者不需要在這上面關注太多。

缺點

  • 死鎖風險:悲觀鎖在使用不當的情況下可能導致死鎖。如果多個事務持有鎖並相互等待對方釋放鎖的情況發生,就可能發生死鎖。
  • 效能較低:悲觀鎖通常需要在整個事務過程中鎖定資源,這可能導致其他事務阻塞。

簡單測試:模擬併發情況下防超賣,假設cs表的num1欄位為庫存欄位

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 select num1 from cs where id = 1 for update;

update cs set num1 = num1 - 6 where id = 1 and num1 >= 6;
select num1 from cs where id = 1 for update;

update cs set num1 = num1 - 8 where id = 1 and num1 >= 8;
雙方新增X行鎖,並準備扣減庫存
3 / 阻塞 此時必有一個事務阻塞,等這個事務扣完庫存提交事務後,在執行另一個扣庫存的事務
4 commit; commit; 雙方提交事務,雙方程式碼執行成功,但會話B庫存不足,最終剩餘庫存4,結束流程

注意,實際的開發邏輯,與以上示例況有偏差。
若前端檢測到庫存不足,直接攔截使用者的下單動作並提示,就不會有後端的流程。
若前端認為庫存充足,後端可能是下單時預扣庫存,預扣庫存的環節提前就檢測當前其庫存是否夠當前的購買數量,若不夠,則直接終止流程,返回使用者結果。
若庫存夠用但未支付,若超時30分鐘未支付則取消訂單(這個延時佇列可以用Redis Zset 或者RabbitMQ等其它訊息中介軟體實現),若支付,則算一筆成功的交易。
這是MySQL能抗住的情況,如果抗不住,就需要Redis+Lua的單執行緒事務來阻止超賣的發生。

樂觀鎖

之前寫過詳細的文章可進行參考:MySQL樂觀鎖與悲觀鎖

樂觀鎖很樂觀,假設資料一般情況不會造成衝突,屬於程式層面的邏輯鎖,在資料進行更新時,才進行鎖的檢測。是透過新增一個版本號的方式實現的,每當資料這一行所在的資料發生變化,則對應的版本號+1,更新資料時,將版本號作為查詢條件。
至於是否要加事務,看寫操作單條資料還是寫操作多條資料。

注意:網上很多解決方案用時間戳來做version欄位,我持反對意見,併發可能是一瞬間的事,不到一秒就有好多請求,用時間戳粒度太大,用隨機字串都比用這個強。

適用場景

  • 讀多寫少:由於併發寫操作較少,樂觀鎖的修改資料受影響行數為0機率也較低。
    允許一定量的重試或不需要重試的場景:這個要根據業務,否則來回重試會降低效能。
    優點
  • 實現簡單:樂觀鎖在程式碼上就可以實現,不需要額外對資料庫額外操作。
  • 無死鎖風險:悲觀鎖有死鎖風險,樂觀鎖沒有。
    無需重試情況下,效能較高:樂觀鎖機制在併發訪問情況下,不需要像悲觀鎖那樣阻塞其他事務,提供了更高的併發效能,前提當前業務需求能容忍寫操作失敗的情況。

缺點

  • 併發衝突:多加了一個where條件,只能保證資料最終不會出錯,不能保證每條寫操作的SQL都執行成功(也就是受影響行數>0)。
  • 不提供強一致性:強一致性要求資料的狀態在任何時刻都保持一致,悲觀鎖是到寫操作那一步才去驗證,期間只是做了個where條件的過濾。
  • ABA問題:一個欄位的值在請求X中查詢出來是A,後續程式碼實現樂觀鎖,因為併發量大,同時過來一個Y請求,將A值改成了B,因為一些業務原因又改成了A,整個過程雖然不影響請求X的結果,且能正常執行,但是聯合其它資料,這個情況是否符合業務場景,不好說,所以最好的解決方案,就是專門做一個version欄位,且不會與之前的version重複,即可,把這個version欄位作為where條件,而不是存A或者B欄位的所在欄位作為where條件。
  • 當前的請求中了上一個樂觀鎖的招,導致的版本號不一樣,需要重試,反覆的重試也降低效能,由於這個原因,所以用於讀操作多的場景。

簡單測試:模擬併發情況下防超賣,假設cs表的num1欄位為庫存欄位,s1為version欄位,一共10個庫存,兩個請求查詢到的庫存都是10,一個請求下單6個,一個請求下單8個。

步驟 會話A 會話B 備註
1 select * from cs where id = 1 select * from cs where id = 1 雙方獲取版本號和庫存
2 update cs set num1 = num1 - 6,s1 = 'version2' where id = 1 and s1 = 'version1' and num1 >=6; update cs set num1 = num1 - 8,s1 = 'version1' where id = 1 and s1 = 'version1' and num1 >=8; 樂觀鎖無需事務,但是需要多個原子性的業務場景,仍推薦加事務
3 扣庫存成功 雖然是併發場景,但A執行略快於B,此時版本號已修改為version2 會話B執行成功,但庫存扣減失敗
4 / select * from cs where id = 1 會話B進行重試,並重新獲取版本號和庫存
5 / update cs set num1 = num1 - 8,s1 = 'version3' where id = 1 and s1 = 'version2' and num1 >=8; 雙方程式碼執行成功,但會話B庫存不足,最終剩餘庫存4,結束流程

隱式鎖

隱式鎖不需要顯式地編寫鎖定關鍵詞,會自動加鎖或解鎖。
例如在未提交的事務中插入資料,就是透過隱式鎖的方式避免其它事務對插入資料的讀寫,如果能讀到了那就是髒讀,如果能寫那就是髒寫。
隱式鎖是一種延時載入的方式,在多個事務的情況下,在第二個事務訪問時,才會被動的加鎖,這樣可以減少鎖的數量。
由於不需要手動處理,瞭解機制就行。

步驟 會話A 會話B 會話C 備註
1 start transaction; start transaction; SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; AB開啟事務,C檢測鎖
2 insert into cs(num1, num2, s1, s2) values(1,2,3,4); / SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; A插入資料,此時C未檢測到有鎖
3 / select * from cs lock in share mode; SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; insert操作會產生一個寫鎖,進而阻止事務B的讀鎖,此時事務C可以檢視的到
4 / 阻塞 / 在第二個事務訪問時,事務A被動的加鎖,透過事務C可以檢視的到
5 commit; commit; / 雙方提交事務,會話A成功插入一條資料

顯式鎖

顯式宣告的鎖,for update,lock in share mode這種的。

全域性鎖

全域性鎖(Global Lock)的作用是鎖定整個資料庫例項,讓整個庫只能讀不能寫,粒度很大。
flush tables with read lock:獲取全域性讀鎖,對所有事務只能讀不能寫。
unlock tables:釋放全域性鎖。
應用場景:備份資料庫或者匯出資料時,為了保證資料的一致性而需要鎖定整個資料庫例項,確保備份過程中資料不會發生變化。全域性鎖會影響資料庫的正常執行。
測試:

步驟 會話A 會話B 備註
1 flush tables with read lock; / 會話A開啟全域性鎖
2 / select * from cs;
desc cs;
讀資料表和表資料都沒問題
3 / insert into cs(num1, num2, s1, s2) values(1,2,3,4); 會話B插入資料
4 / 阻塞 全域性鎖禁止寫操作
5 unlock tables; 插入成功 釋放全域性鎖。資料插入成功
6 flush tables with read lock; / 會話A開啟全域性鎖
7 / alter table cs add index(num1); 會話B為表欄位加索引
8 / 阻塞; 全域性鎖阻塞DDL操作
9 unlock tables; 索引新增成功 釋放全域性鎖,索引插入成功,結束會話

死鎖

之前寫過關於死鎖的詳細文章:MySQL死鎖
MySQL 中的死鎖是指兩個或多個事務相互等待對方釋放鎖資源,導致它們永遠無法繼續執行的情況。(不會耕田,怎能下地 不會下地,怎麼耕田)
測試:先cs表中有id為1,6的兩條資料

步驟 會話A 會話B 備註
1 start transaction; start transaction; 雙方開啟事務
2 update cs set num1 = 1 where id = 1; update cs set num1 = 60 where id = 6; 更新資料預設會產生X鎖,這一步正常執行
3 update cs set num1 = 6 where id = 6; update cs set num1 = 10 where id = 1; 更新資料預設會產生X鎖
4 阻塞 / 事務B對於id=6的寫鎖並未釋放,此時又新增了update請求,需要加X鎖,所以阻塞
5 / Deadlock found when trying to get lock; try restarting transaction 事務AB互相需要等待,導致死鎖
6 commit; commit; 雙方提交事務,結束流程,事務A正確執行,事務B被回滾

排查方法1

show engine innodb status;

執行以上命令會得到大量日誌,在LATEST DETECTED DEADLOCK與TRANSACTIONS之間尋找sql 語句,以此定位死鎖源頭。示例如下:

......
------------------------
LATEST DETECTED DEADLOCK
------------------------
2024-01-23 23:48:30 0x1f00
*** (1) TRANSACTION:
TRANSACTION 805714, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 3, OS thread handle 8076, query id 762 localhost ::1 root updating
update test set name = 'C++語言' where id = 2
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2575 page no 3 n bits 72 index PRIMARY of table `temp`.`test` trx id 805714 lock_mode X locks rec but not gap waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000002; asc     ;;
 1: len 6; hex 0000000c4b53; asc     KS;;
 2: len 7; hex 34000002a80923; asc 4     #;;
 3: len 3; hex 432b2b; asc C++;;

*** (2) TRANSACTION:
TRANSACTION 805715, ACTIVE 9 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 4, OS thread handle 7936, query id 766 localhost ::1 root updating
delete from test where id = 1
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 2575 page no 3 n bits 72 index PRIMARY of table `temp`.`test` trx id 805715 lock_mode X locks rec but not gap
Record lock, heap no 6 PHYSICAL RECORD: n_fields 4; compact format; info bits 32
 0: len 4; hex 00000002; asc     ;;
 1: len 6; hex 0000000c4b53; asc     KS;;
 2: len 7; hex 34000002a80923; asc 4     #;;
 3: len 3; hex 432b2b; asc C++;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 2575 page no 3 n bits 72 index PRIMARY of table `temp`.`test` trx id 805715 lock_mode X locks rec but not gap waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 00000001; asc     ;;
 1: len 6; hex 0000000c4b46; asc     KF;;
 2: len 7; hex ad000002b10110; asc        ;;
 3: len 1; hex 43; asc C;;

*** WE ROLL BACK TRANSACTION (1)
------------
TRANSACTIONS
------------
Trx id counter 805727
Purge done for trx's n:o < 805727 undo n:o < 0 state: running but idle
History list length 36
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 283132143509864, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 283132143508992, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 283132143508120, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 283132143507248, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
......

排查方法2

show status like 'innodb_row_lock%'

執行以上命令後會得到一個表格,

Innodb_row_lock_current_waits	0    如果這個數字非零,表示有事務正在等待其他事務持有的行級鎖。
Innodb_row_lock_time	       18756 以毫秒為單位。它表示所有事務在競爭鎖資源時,所消耗的總時間
Innodb_row_lock_time_avg	   3126  平均每個InnoDB行級鎖的持有時間,毫秒為單位。用總的鎖持有時間除以鎖請求次數來計算。
Innodb_row_lock_time_max	   7921  單個InnoDB行級鎖的最大持有時間,毫秒為單位。這個值表示所有事務競爭鎖資源中的最長時間。
Innodb_row_lock_waits      	   6     這個指標表示有多少次事務在競爭鎖資源時需要等待。

排查方法3
該查詢是用來獲取當前正在執行的事務(INNODB_TRX表)、空閒狀態的執行緒(PROCESSLIST表,COMMAND為Sleep)、執行緒對應的程序資訊(threads表)、執行緒對應的當前執行中的SQL語句(events_statements_current表)的一些相關資訊。

SELECT a.trx_id, a.trx_state, a.trx_started, a.trx_query, b.ID, b.USER, b.DB, b.COMMAND, b.TIME, b.STATE, b.INFO, c.PROCESSLIST_USER, c.PROCESSLIST_HOST, c.PROCESSLIST_DB, d.SQL_TEXT 
FROM information_schema.INNODB_TRX a
LEFT JOIN information_schema.PROCESSLIST b ON a.trx_mysql_thread_id = b.id AND b.COMMAND = 'Sleep'
LEFT JOIN performance_schema.threads c ON b.id = c.PROCESSLIST_ID
LEFT JOIN performance_schema.events_statements_current d ON d.THREAD_ID = c.THREAD_ID;

解決
MySQL會自動解決死鎖問題,代價就是MySQL自行找到成本最低的事務,自動回滾。所以說解決方案不是解鎖,而是避免。

避免

  • 降低事務粒度:輕量級的事務,鎖定更少的資源,不容易發生死鎖。
  • 儘快提交事務:鎖能更快的釋放。
  • 合理的索引設計: 合理設計資料庫表的索引可以減少鎖競爭,提高查詢效率。
  • 一致的訪問順序: 當應用程式涉及多個表時,保持一致的訪問順序可以避免死鎖。例如,如果事務A先鎖定表X,再鎖定表Y,那麼事務B也應該按照相同的順序鎖定表X和表Y,從而避免死鎖。

什麼是最小成本回滾策略?
在 MySQL 中,當發生死鎖時,MySQL 使用一種叫做"最小成本回滾"(InnoDB 中稱為"最小編號事務回滾")的策略來選擇一個事務作為犧牲者並進行回滾,最小成本回滾策略是 MySQL 的預設行為,它會自動選擇犧牲者並回滾事務。
最小成本回滾策略的原理是選擇最小成本的事務作為犧牲者。評估演算法如下:

  • 回滾事務所涉及的運算元量:回滾操作的數量越小,成本越低。
  • 回滾事務所佔用的系統資源:回滾事務佔用的系統資源越少,成本越低。
  • 回滾事務已執行的工作量:已執行的工作量越少,成本越低。

鎖排查與監控

查詢語句用於檢視 InnoDB 儲存引擎中關於行鎖的相關統計資訊

show status like 'innodb_row_lock%';

Innodb_row_lock_current_waits  如果這個數字非零,表示有事務正在等待其他事務持有的行級鎖。
Innodb_row_lock_time	       以毫秒為單位。它表示所有事務在競爭鎖資源時,所消耗的總時間
Innodb_row_lock_time_avg	   平均每個InnoDB行級鎖的持有時間,毫秒為單位。用總的鎖持有時間除以鎖請求次數來計算。
Innodb_row_lock_time_max	   單個InnoDB行級鎖的最大持有時間,毫秒為單位。這個值表示所有事務競爭鎖資源中的最長時間。
Innodb_row_lock_waits      	   這個指標表示有多少次事務在競爭鎖資源時需要等待。

檢視 InnoDB 儲存引擎中當前活動的事務資訊

SELECT * FROM information_schema.innodb_trx;

trx_id                         事務的唯一識別符號。
trx_state                      事務的狀態,如 RUNNING、LOCK WAIT、ROLLING BACK 等。
trx_started                    事務啟動的時間。
trx_requested_lock_id          請求的鎖的識別符號。
trx_wait_started               等待鎖的開始時間。
trx_weight                     事務的權重,用於死鎖檢測。
trx_mysql_thread_id            MySQL 執行緒 ID。
trx_query                      與事務相關的 SQL 查詢語句。
trx_operation_state            事務內部操作的狀態。
trx_tables_in_use              事務使用的表的數量。
trx_tables_locked              事務鎖定的表的數量。
trx_lock_structs               事務內部使用的鎖結構數量。
trx_lock_memory_bytes          用於事務鎖定的記憶體位元組數。
trx_rows_locked                事務鎖定的行數。
trx_rows_modified              事務修改的行數。
trx_concurrency_tickets        用於事務併發控制的票數。
trx_isolation_level            事務的隔離級別。
trx_unique_checks              是否啟用了唯一性檢查。
trx_foreign_key_checks         是否啟用了外來鍵約束檢查。
trx_last_foreign_key_error     最後一個外來鍵錯誤資訊。
trx_adaptive_hash_latched      是否適應性雜湊被鎖定。
trx_adaptive_hash_timeout      適應性雜湊鎖定超時次數。
trx_is_foreign_key_with_check  是否用於外來鍵約束檢查。
trx_is_foreign_key             是否用於外來鍵約束。

檢視 InnoDB 儲存引擎中當前正在被鎖定的物件(如表、行)的資訊

SELECT * FROM information_schema.innodb_locks;

lock_id      鎖的唯一識別符號。
lock_trx_id  持有該鎖的事務的唯一識別符號。
lock_mode    鎖的模式,如 S(共享鎖)或 X(獨佔鎖)等。
lock_type    鎖的型別,如 RECORD(行級鎖)或 TABLE(表級鎖)等。
lock_table   被鎖定的表名。
lock_index   被鎖定的索引名。
lock_space   被鎖定的表空間的識別符號。
lock_page    被鎖定的頁碼。
lock_rec     鎖定的記錄。
lock_data    與鎖相關的其他資料。

檢視 InnoDB 儲存引擎中當前存在的鎖等待情況

SELECT * FROM information_schema.innodb_lock_waits;

requesting_trx_id   請求鎖的事務的唯一識別符號。
requested_lock_id   請求的鎖的唯一識別符號。
blocking_trx_id     導致鎖等待的阻塞事務的唯一識別符號。
blocking_lock_id    導致鎖等待的鎖的唯一識別符號。

相關文章