作者:IT王小二
MySQL是怎麼解決併發事務所產生的問題呢?又藉助了哪些鎖的思想呢?這篇小二給小夥伴們繼續嘮一嘮MySQL的那些事。
一、事務是什麼
事務是由一組SQL語句組成的邏輯處理單元,這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。通常有下面4個特性,也就是我們程式猿俗稱的ACID屬性。
- 原子性(Atomicity):事務是一個原子操作單元,其對資料的修改,要麼全部執行,要麼全部不執行。舉個例子:情人節小二給女朋友轉賬520塊,那麼SQL中一共兩個操作,小二的賬戶餘額
-520
塊,女朋友賬戶餘額+520
塊,這一組轉賬操作,不能只執行小二的賬戶-520
塊,或者只執行女朋友賬戶餘額+520
塊,只能都執行,或者都不執行。 - 一致性(Consistent):在事務開始和完成時,資料都必須保持一致狀態。一致性和原子性息息相關,即一組轉賬操作下來小二賬戶餘額應該
-520
塊,女朋友賬戶餘額+520
塊,兩人轉賬操作之後賬戶總金額是一致的。 - 隔離性(Isolation):一個事務的執行不能被其他事務干擾,即每個事務都是獨立的,不受其他事務的影響。簡單來說就是一個事務(T1)處理過程的中間狀態對其他事務(T2, T3...)是不可見的;同理,T2同樣對T1和T3的中間狀態不可見的。即只要小二的轉賬操作沒完成,那麼女朋友不管怎麼查詢結果都是原來的餘額,而不會查詢多出
520
塊。 - 永續性(Durable):事務完成之後,對資料的修改是永久性的,即便是在資料庫系統遇到故障的情況下也不會丟失提交事務的操作。即只要小二轉賬成功了,資料就儲存磁碟了。
二、併發事務所產生的問題
髒寫或者更新丟失(Lost Update)
當兩個或多個事務同一時間修改同一行,然後基於最初選定的值進行業務操作更新該行時,由於每個事務都不知道其他事務的存在,就會發生丟失更新問題。
簡單來說就是最後的更新覆蓋了由其他事務所做的更新。
髒讀(Dirty Reads)
一個事務讀到了其他事務已經修改但是未提交的資料,未提交意味著這些資料可能會回滾,也就是可能最終不一定會存到資料庫中,所以讀出來的資料是無效的。
不可重複讀(Non-Repeatable Reads)
一個事務在讀取某些資料後的某個時間點,再次讀取以前讀過的那批資料,卻發現其讀出的資料已經發生了改變,可能會受到其他事務的影響,比如其他事務改了這批資料並提交了。通常針對資料更新(UPDATE)操作或者刪除(DELETE)操作。
簡單來說就是在一個事務中,相同查詢語句在不同時刻查詢出來的結果不一致,可能是結果欄位值不一致,也可能是少了行資料。
幻讀(Phantom Reads)
對比不可重複讀,幻讀是針對資料新增(INSERT)來說的,在同一事務下,在讀取某些資料後的某個時間點,再次讀取以前讀過的那批資料,第二次的SQL語句返回了之前不存在的行。
簡單來說就是事務A讀取到了其他事務提交的新增資料。
再來區分一下經常搞混的不可重複讀和幻讀。
- 不可重複讀:說的是原來存在的記錄A,記錄A從A變成了記錄B。
- 幻讀:出現了原來不存在的記錄。
當然還是區分不明顯的小夥伴可以結合五、行鎖與事務隔離級別案例分析理解理解,理論結合實踐,nice~
三、事務隔離級別
併發事務產生的髒讀、不可重複讀、幻讀都是資料庫讀取的一致性,問題,所以資料庫提供了一定的事務隔離級別來解決產生的問題。
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
讀未提交(read-uncommitted) | 是 | 是 | 是 |
不可重複讀(read-committed) | 否 | 是 | 是 |
可重複讀(repeatable-read) | 否 | 否 | 是 |
序列化(serializable) | 否 | 否 | 否 |
MySQL資料庫預設的隔離級別為可重複讀(repeatable-read),也就是我們常說的RR級別。
檢視事務隔離級別:show variables like 'tx_isolation';
設定事務隔離級別,例如設定隔離級別為讀未提交:
- 僅對當前會話生效,立即生效:
set session transaction isolation level read uncommitted;
或者set tx_isolation = 'read-uncommitted';
- 對全域性會話生效,需要退出會話後生效:
set global transaction isolation level read uncommitted;
用Spring開發程式時,如果不設定隔離級別預設用MySQL設定的隔離級別,如果Spring設定了就用設定的隔離級別。
四、鎖分類
鎖是計算機協調多個程式或者執行緒併發訪問同一資源的機制,而對於資料庫來說資料就是一種需要使用者共享的資源,怎麼保證資料的併發訪問一致性和高效性是資料庫需要解決的問題。
按不同分類有以下分類。
- 從效能上來分,分為樂觀鎖和悲觀鎖。
- 樂觀鎖比較樂觀,樂觀鎖假設資料一般情況不會造成衝突,多執行緒同時對同一行資料修改的時候,在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果衝突,則返回給使用者異常資訊,讓使用者決定如何去做。
- 悲觀鎖比較悲觀,多執行緒同時對同一行資料修改的時候,最終只有一個執行緒修改成功。
- 從對資料庫操作的型別分,分為讀鎖和寫鎖(都屬於悲觀鎖)。
- 讀鎖(共享鎖,S鎖(Shared)):針對同一份資料,多個讀操作可以同時進行而不會互相影響。
- 寫鎖(排它鎖,X鎖(eXclusive)):當前寫操作沒有完成前,它會阻斷其他寫鎖和讀鎖。
- 從對資料操作的粒度分,分為表鎖和行鎖。
1. 表鎖
每次操作鎖住整張表。開銷小,加鎖快,不會出現死鎖,鎖定粒度大,發生鎖衝突的概率最高,併發度最低,一般用在整表資料遷移的場景。
基本操作
- 手動增加表鎖:
lock table 表名稱 read/write,表名稱2 read/write;
- 檢視錶上加過的鎖:
show open tables;
- 刪除表鎖:
unlock tables;
實操一下
-- 建立示例表
CREATE TABLE `test_myisam_lock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`age` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = MyISAM DEFAULT CHARSET = utf8;
-- 插入幾條資料
INSERT INTO `blog_test`.`test_myisam_lock` (`id`, `name`, `age`) VALUES (1, 'itwxe', 18);
INSERT INTO `blog_test`.`test_myisam_lock` (`id`, `name`, `age`) VALUES (2, 'Lee Patel', 62);
INSERT INTO `blog_test`.`test_myisam_lock` (`id`, `name`, `age`) VALUES (3, 'Sakurai Mio', 55);
INSERT INTO `blog_test`.`test_myisam_lock` (`id`, `name`, `age`) VALUES (4, 'Tan Xiuying', 42);
INSERT INTO `blog_test`.`test_myisam_lock` (`id`, `name`, `age`) VALUES (5, 'Cheng Yunxi', 47);
1、加讀鎖
本文表格中序號代表兩個session中SQL的執行順序,同一行為從左往右執行的語句。圖中已標註當前示例SQL執行順序,後續不再截圖說明。
序號 | session1 | session2 |
---|---|---|
1 | lock table test_myisam_lock read; | |
2 | select * from test_myisam_lock; -- 可以查詢 |
select * from test_myisam_lock; -- 可以查詢 |
3 | insert into blog_test.test_myisam_lock ( name, age) values ('xiaowu', 18); -- 插入失敗,報錯 |
insert into blog_test.test_myisam_lock ( name, age) values ('xiaowu', 18); -- 等待執行 |
4 | unlock tables; | -- 釋放鎖後沒有超時則執行插入操作 |
對MyISAM表的讀操作(加讀鎖),不會阻塞其他程式對同一表的讀請求;但會阻塞對同一表的寫請求,當前執行緒無法執行寫操作,其他執行緒進入等待狀態,只有當讀鎖釋放後,才會執行其它程式的寫操作。
2、加寫鎖
序號 | session1 | session2 |
---|---|---|
1 | Lock table test_myisam_lock write; | |
2 | select * from test_myisam_lock; -- 可以查詢 |
select * from test_myisam_lock; -- 進入等待執行狀態,手動取消(ctrl + c) |
3 | insert into blog_test.test_myisam_lock ( name, age) values ('xiaoer', 18); -- 插入成功 |
insert into blog_test.test_myisam_lock ( name, age) values ('xiaoer', 18); -- 進入等待執行狀態,手動取消(ctrl + c) |
4 | update blog_test.test_myisam_lock set name = 'wangxiaoer' where id = 8; -- 更新成功 |
update blog_test.test_myisam_lock set name = 'wangxiaoer' where id = 8; -- 進入等待執行狀態,手動取消(ctrl + c) |
5 | delete from blog_test.test_myisam_lock where id = 8; -- 刪除成功 |
delete from blog_test.test_myisam_lock where id = 8; -- 進入等待執行狀態 |
6 | unlock tables; | -- 釋放鎖後沒有超時則進行刪除操作 |
簡單來說:對MylSAM表的寫操作(加寫鎖),不會阻塞當前程式對錶的讀寫操作;但是會阻塞其他程式對同一表的讀和寫操作,只有當寫鎖釋放後,才會執行其它程式的讀寫操作。
2. 行鎖
每次操作鎖住一行資料,開銷大,加鎖慢,會出現死鎖,鎖定粒度最小,發生鎖衝突的概率最低,併發度最高。
同時InnoDB與MyISAM最大的兩點不同就是支援事務和支援行級鎖。
基本操作
-- 建立示例表
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 插入幾條資料
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (1, 'itwxe', 1000);
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (2, 'Kenneth Adams', 2000);
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (3, 'Takada Daichi', 3000);
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (4, 'Anne Russell', 4000);
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (5, 'Jia Yuning', 5000);
預設的RR隔離級別下示例,即可重複讀(repeatable-read)。
序號 | session1 | session2 |
---|---|---|
1 | begin; -- 開啟事務 |
begin; -- 開啟事務 |
2 | update account set balance = balance + 1000 where id = 1; -- 更新id = 1的餘額 |
select * from account where id = 1; -- 查詢不阻塞,正常查詢 |
3 | update account set balance = balance + 1000 where id = 1; -- 更新操作阻塞,進入等待執行狀態,最後會超時或者等待session1提交(回滾)事務後才會執行更新語句。 |
|
4 | commit; -- 提交事務 |
-- session1提交(回滾)事務後,沒有超時則進行更新操作 |
當然啦,小二這裡寫的只有兩個會話,如果有很多個session同時更新一行資料也是一樣進入等待狀態。
簡單來說:在非序列化事務隔離級別下,InnoDB在執行查詢語句select時不會加鎖,但是update、insert、delete操作會加行鎖。
當然,當查詢語句也需要加鎖也是可以的,使用 select ... for update
。
select * from account where id = 1 for update;
此時當前會話對於id = 1
的行可以進行增刪改查操作,其它會話不能對id = 1
行進行操作。只有等待當前會話釋放鎖(提交/回滾事務)後才能夠操作記錄id = 1
的行。
五、行鎖與事務隔離級別案例分析
行鎖支援事務所以當然就得結合事務隔離級別來分析實操一下。
1. 讀未提交
首先將示例賬戶餘額還原到1000,update account set balance = 1000 where id = 1;
。
序號 | session1 | session2 |
---|---|---|
1 | set tx_isolation = 'read-uncommitted'; -- 設定當前回話事務隔離級別為讀未提交 |
set tx_isolation = 'read-uncommitted'; -- 設定當前回話事務隔離級別為讀未提交 |
2 | begin; -- 開啟事務 |
|
3 | select * from account where id = 1; -- 第一次查詢餘額為1000 |
begin; -- 開啟事務 |
4 | update account set balance = balance + 1000 where id = 1; -- 更新id = 1的餘額 |
|
5 | select * from account where id = 1; -- 第二次查詢餘額為2000,這裡就是髒讀了,讀到了沒有提交的資料 |
|
6 | rollback; -- 回滾 |
|
7 | select * from account where id = 1; -- 第三次查詢餘額為1000 |
|
8 | update account set balance = balance - 2000 where id = 1; -- 例如序號5中的餘額在Java程式碼中判斷可以購買後執行了扣減餘額 |
|
9 | commit; -- 提交事務 |
可以看到,讀未提交隔離級別下,第5行session讀取到的賬戶餘額就是2000了,這個時候如果Java應用程式使用2000作為判斷了某些業務操作,例如判斷是否可以購買一個2000的商品,此時session又還沒有扣減餘額,那麼當session2因為某些原因回滾了,此時id為1的使用者餘額就是 -1000 塊了,這是明顯不合理的;並且實際開發中一定不要使用Java程式計算後直接將值更新到資料庫,這樣會造成髒寫,例如Java程式計算值後賬戶餘額為零,那麼執行update account set balance = 0 where id = 1;
的話問題就更大了。
為了解決這個問題就要用到讀已提交的隔離級別了。
2. 讀已提交
首先將示例賬戶餘額還原到1000,update account set balance = 1000 where id = 1;
。
序號 | session1 | session2 |
---|---|---|
1 | set tx_isolation = 'read-committed'; -- 設定當前回話事務隔離級別為讀已提交 |
set tx_isolation = 'read-committed'; -- 設定當前回話事務隔離級別為讀已提交 |
2 | begin; -- 開啟事務 |
|
3 | select * from account where id = 1; -- 第一次查詢餘額為1000 |
begin; -- 開啟事務 |
4 | update account set balance = balance + 1000 where id = 1; -- 更新id = 1的餘額 |
|
5 | select * from account where id = 1; -- 第二次查詢餘額為1000 |
|
6 | commit; -- 提交事務 |
|
7 | select * from account where id = 1; -- 第三次查詢餘額為2000 |
|
8 | commit; -- 提交事務 |
可以看到已經解決了髒讀的問題,但是在session2提交後出現了不可重複讀的問題,不可重複讀會讓我們在編寫Java應用程式碼的時候很難編寫,而不可重複讀就得靠可重複讀的隔離級別來解決了。
3. 可重複讀
首先將示例賬戶餘額還原到1000,update account set balance = 1000 where id = 1;
。
序號 | session1 | session2 |
---|---|---|
1 | set tx_isolation='repeatable-read'; -- 設定當前回話事務隔離級別為可重複讀 |
set tx_isolation='repeatable-read'; -- 設定當前回話事務隔離級別為可重複讀 |
2 | begin; -- 開啟事務 |
|
3 | select * from account; -- 第一次查詢id = 1賬戶餘額為1000 |
begin; -- 開啟事務 |
4 | update account set balance = balance + 1000 where id = 1; -- 更新id = 1的餘額 |
|
5 | select * from account; -- 第二次查詢id = 1賬戶餘額為1000 |
|
6 | commit; -- 提交事務 |
|
7 | select * from account; -- 第二次查詢id = 1賬戶餘額為1000,可以看到已經解決了不可重複讀的問題 |
|
8 | begin; -- 重新開啟一個事務,驗證幻讀問題 |
|
9 | insert into account (id, name, balance) values (6, 'xiaoer', 6000); -- 插入一行資料 commit; -- 提交事務 |
|
10 | select * from account; -- 結果和序號7中展示的結果一致,只有id = 1~5的記錄 |
|
11 | update account set balance = 6666 where id = 6; -- 更新新增的id = 6記錄 |
|
12 | select * from account; -- 再次發現查詢到的記錄多了id = 6的記錄,且id=6的賬戶餘額為6666 |
|
13 | commit; |
所以,可以看到其實MySQL並沒有完全解決幻讀的問題,在RR級別下當MySQL有可能會出現幻讀問題。
當然我們實際開發通常也不會這樣去操作,因為id = 6
這個對當前session是不可見的,所以通常不會直接修改到其他session插入的記錄。
所以MySQL預設的事務隔離級別就是RR級別,併發高且能解決99.99%的業務場景。
4. 序列化
首先將示例賬戶餘額還原到1000,update account set balance = 1000 where id = 1;
。
序號 | session1 | session2 |
---|---|---|
1 | set tx_isolation='serializable'; -- 設定當前回話事務隔離級別為序列化 |
set tx_isolation='serializable'; -- 設定當前回話事務隔離級別為序列化 |
2 | begin; -- 開啟事務 |
begin; -- 開啟事務 |
3 | select * from account where id = 1; -- 查詢id = 1資訊,賬戶餘額為1000 |
select * from account where id = 2; -- id = 2的記錄可以查詢 |
4 | select * from account where id = 1; -- id = 1賬戶餘額為1000 |
|
5 | select * from account where id = 2; -- 進入等待執行狀態,最後會超時或者等待session1提交事務或者回滾後才會執行查詢。 |
|
6 | commit; | -- 如果沒有超時則執行查詢id = 2的記錄資訊 |
7 | commit; |
可以看到序列化隔離級別下,使用的是排它鎖,鎖定記錄的增刪改查都會收到影響進入等待執行狀態。
所以,序列化隔離級別下不會有幻讀的問題,當然效率也最低,通常實際專案開發中都不會使用。
六、行鎖詳解
1. 記錄鎖、間隙鎖、臨鍵鎖
行鎖有三個型別,分別是記錄鎖、間隙鎖、臨鍵鎖,記錄鎖只鎖定一行記錄,而間隙鎖和臨鍵鎖都是鎖定一個範圍的記錄行。
記錄鎖(Record Locks)
記錄鎖鎖定一行記錄的已經在鎖分類中演示過了 id = 1
的場景,不嘮叨了。
間隙鎖(Gap Locks)
間隙鎖指的是鎖的是兩個值之間的間隙,可以解決RR級別下一些情景的幻讀問題。
-- 再插入幾條資料演示間隙鎖和臨鍵鎖
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (13, 'itwxe.com', 13000);
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (18, 'zhangsan', 18000);
INSERT INTO `blog_test`.`account` (`id`, `name`, `balance`) VALUES (25, 'lisi', 25000);
插入資料後id就產生了(6, 13), (13, 18), (18, 25), (25, 正無窮)
這四個空間間隙。
下面使用RR事務隔離級別,set tx_isolation='repeatable-read';
。
序號 | session1 | session2 |
---|---|---|
1 | begin; -- 開啟事務 |
begin; -- 開啟事務 |
2 | update account set balance = balance + 1000 where id > 8 and id < 13; -- 查詢製造間隙(8, 13) |
insert into account values(6, 'jianxi', 6000); -- 可以正常執行響應結果,但是主鍵已存在,插入失敗 |
3 | insert into account values(7, 'jianxi', 7000); -- 插入id = 10的記錄會發現插入不了,進入等待執行狀態 |
|
4 | insert into account values(10, 'jianxi', 8000); -- 插入id = 10的記錄會發現插入不了,進入等待執行狀態,手動取消(ctrl + c) |
|
5 | insert into account values(13, 'jianxi', 8000); -- 插入id = 13同樣會阻塞,進入等待執行狀態 |
|
6 | insert into account values(14, 'jianxi', 8000); -- 可以正常插入 |
|
7 | rollback; -- 回滾事務 |
rollback; -- 回滾事務 |
可以看到測試中產生鎖定的範圍為(6, 13]
,也就是說間隙鎖鎖定的範圍是小範圍(id=8)前一條行記錄(id=6)的開區間到大範圍(id=13)的後一條記錄,左開右閉區間。
可以看到間隙鎖在減少了某些情況下幻讀的發生,通過對間隙加鎖讓間隙內在session1提交(回滾)事務之前其他session都無法進行插入或者修改任何資料。
間隙鎖是在可重複讀隔離級別下才會生效。
臨鍵鎖(Next-key Locks)
臨鍵鎖可以說是特殊的間隙鎖,即行鎖和間隙鎖的結合,可以理解為間隙鎖中包含記錄行。
目前存在(6, 13), (13, 18), (18, 25), (25, 正無窮)
這四個空間間隙。
下面使用RR事務隔離級別,set tx_isolation='repeatable-read';
。
序號 | session1 | session2 |
---|---|---|
1 | begin; -- 開啟事務 |
begin; -- 開啟事務 |
2 | update account set balance = balance + 1000 where id > 15 and id < 28; -- 查詢製造間隙(8, 13) |
insert into account values(13, 'linjian', 13000); -- 可以正常執行響應結果,但是主鍵已存在,插入失敗 |
3 | insert into account values(17, 'linjian', 17000); -- 插入id = 17的記錄會發現插入不了,進入等待執行狀態 |
|
4 | insert into account values(18, 'linjian', 18000); -- 插入id = 18的記錄會發現插入不了,進入等待執行狀態,這就是包含的行鎖。手動取消(ctrl + c) |
|
5 | insert into account values(25, 'linjian', 25000); -- 插入id = 25同樣會阻塞,進入等待執行狀態 |
|
6 | insert into account values(29, 'linjian', 29000); -- 插入id = 29同樣會阻塞,進入等待執行狀態 |
|
7 | rollback; -- 回滾事務 |
|
8 | -- 如果這個時候還沒有超時則執行插入id = 29的記錄 | |
9 | rollback; -- 回滾事務 |
可以看到臨鍵鎖鎖定的範圍是(13,正無窮]
。同樣是左開右閉,取不到13而取得到正無窮。
2. 無索引行鎖升級表鎖
序號 | session1 | session2 |
---|---|---|
1 | begin; -- 開啟事務 |
begin; -- 開啟事務 |
2 | update account set balance = balance + 1000 where name = 'itwxe'; -- 進行更新操作,name列沒有索引 |
select * from account where id = 2; -- 升級表鎖可以查詢 |
3 | insert into account values(14, 'linjian', 13000); -- 升級表鎖無法插入,進入等待狀態 |
|
4 | rollback; -- 回滾事務,當然提交也可以 |
|
5 | -- 如果這個時候還沒有超時則執行插入id = 14的記錄 | |
6 | rollback; -- 回滾事務,當然提交也可以 |
看到這裡都是耐心的小夥伴啦,InnoDB的行鎖是針對索引加的鎖,不是針對記錄加的鎖。並且該索引不能失效,否則會從行鎖升級為表鎖。
所以,如果對非索引欄位更新,行鎖會升級成表鎖,升級成表鎖後任意行的寫操作都會阻塞,直到表鎖被釋放。
3. 行鎖分析
可以通過檢查innodb_row_lock
狀態變數來分析MySQL行鎖的情況。
show status like 'innodb_row_lock%'
- Innodb_row_lock_current_waits:當前正在等待鎖定的數量
- Innodb_row_lock_time:從MySQL啟動到現在鎖定總時間長度
- Innodb_row_lock_time_avg:每次等待的平均時間
- Innodb_row_lock_time_max:MySQL啟動到現在最長的一次等待時間
- Innodb_row_lock_waits:MySQL啟動後到現在總共等待的次數
當等待次數很多,並且每次等待時長也很長時,就要分析系統中為什麼會有這麼多的等待,然後根據分析結果著手製定優化計劃。
4. 系統庫與鎖相關的表
MySQL系統庫中幾個與鎖相關的表,有時開發中debug除錯碰到一直卡著SQL語句不執行,這時候完全有可能是其他小夥伴也在除錯這一行資料,然後一直鎖定了那行資料所以你就除錯不下去了,那麼咋辦嘞?
MySQL給我們提供了表來查詢,只要kill
掉其他小夥伴的事務自然你就可以除錯了,不過輕易不要嘗試,因為你kill
掉那個事務後其他小夥伴如果沒除錯完那麼就白除錯了,所以被揍了小二概不負責?
‐‐ 檢視事務
select * from INFORMATION_SCHEMA.INNODB_TRX;
‐‐ 檢視鎖
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
‐‐ 檢視鎖等待
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
‐‐ 釋放鎖,trx_mysql_thread_id是INNODB_TRX表中查詢的資訊的值,例如`kill 394`,kill掉的事務無法提交也無法回滾,會包ERROR錯誤
kill trx_mysql_thread_id;
‐‐ 檢視鎖等待詳細資訊
show engine innodb status\G;
5. 死鎖
序號 | session1 | session2 |
---|---|---|
1 | set tx_isolation='repeatable-read'; -- 設定當前回話事務隔離級別為可重複讀 |
set tx_isolation='repeatable-read'; -- 設定當前回話事務隔離級別為可重複讀 |
2 | begin; -- 開啟事務 |
begin; -- 開啟事務 |
3 | select * from account where id = 1 for update; -- 正常查詢 |
select * from account where id = 2 for update; -- 正常查詢 |
4 | select * from account where id = 2 for update; -- 進入等待狀態 |
select * from account where id = 1 for update; -- 報錯,檢測到死鎖,重新啟動事務 -- ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
5 | -- 因為session事務被重啟,所以id = 2的行鎖被釋放,所以沒有超時的情況下執行查詢 | rollback; |
6 | rollback; |
大多數情況下MySQL可以自動檢測死鎖並回滾產生死鎖的那個事務,但是有些情況MySQL沒法自動檢測死鎖。
6. 鎖優化建議
- 儘可能讓所有資料檢索都通過索引來完成,避免無索引行鎖升級為表鎖。
- 儘可能減少檢索條件範圍,避免間隙鎖。
- 儘量控制事務大小,減少鎖定資源量和時間長度,涉及事務加鎖的sql儘量放在事務最後執行。