大話MySQL鎖
一、鎖介紹
不同儲存引擎支援的鎖是不同的,比如MyISAM只有表鎖,而InnoDB既支援表鎖又支援行鎖。
下圖展示了InnoDB不同鎖型別之間的關係:
圖中的概念比較多不好理解,下面依次進行說明。
1.1樂觀鎖
樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設資料一般情況下不會造成衝突,所在在資料進行提交更新時,才會對資料的衝突與否進行檢測,如果發現衝突了,則返回給使用者錯誤資訊,讓使用者決定如何處理,其核心是基於CAS演算法。樂觀鎖適用於讀多寫少的場景,可以提高程式吞吐量。
Mysql自帶的是沒有樂觀鎖的,但是可以通過表上加個version欄位來實現自己樂觀鎖。
假如要更新一個使用者的年齡,可以這樣做:
- 查出使用者id等於3的使用者資訊,select id,name,age,version from user where id = 3,得到如下的資料。
id | Name | Age | Version |
---|---|---|---|
3 | 張三 | 26 | 1 |
-
更新張三的年齡為27,注意where條件帶上版本號。update user set age = 27,version = 2 where id = 3 and version = 1;
-
如果更新的結果是1則表示更新成功了,如果是0則表示更新失敗需要重新嘗試。
1.2悲觀鎖
悲傷鎖就是在每次操作資料時,都悲觀地認為會出現資料衝突,所以必須先獲取到資料的鎖再對其修改。傳統的關係型資料庫用的就是悲觀鎖,還有JDK中的synchronized關鍵字等。悲觀鎖主要分為共享鎖和排他鎖。
1.3共享鎖
共享鎖【shared locks】,又叫讀鎖,顧名思義,共享鎖就是多個事務對同一個資料可以共享一把鎖,都能訪問到資料,但是隻能讀不能修改。
如何獲取共享鎖?
select * from user where id = 3 lock in share mode;
注意:在有事務獲取到了共享鎖之後,其他事務是不能做insert/update/delete操作的,因為insert/update/delete語句會自動加上排他鎖。
1.4排他鎖
排他鎖【exclusive locks】,又叫寫鎖,顧名思義,排他鎖就是不能與其他鎖並存,如果一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的其他鎖,包括共享鎖和排他鎖,但是獲取排他鎖的事務是可以對資料進行讀取和修改。
如何獲取排他鎖?
在sql語句後加上for update即可。
select * from user where id = 3 for update
1.5表鎖
表鎖,顧名思義就是對整張表加鎖,是Mysql各儲存引擎中最大粒度的鎖定機制。
優點:實現邏輯簡單,獲取鎖和釋放鎖的速度很快,由於每次都是將整張表鎖定所以可以很好的避免死鎖問題。
缺點:鎖定顆粒度大導致出現鎖定資源爭用的概率高,併發度低。
1.6行鎖
行鎖,顧名思義就是對錶中的某行資料加鎖,鎖定顆粒度最小。
優點:發生鎖衝突的概率低,併發處理能力強。
缺點:由於鎖定資源的顆粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗自然也就更大了。此外,行級鎖定也最容易發生死鎖。
如何判斷使用的是行鎖還是表鎖?
InnoDB的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以只有在通過索引條件檢索資料時才會用行鎖,否則使用表鎖。並且該索引不能失效,否則都會從行鎖升級為表鎖。所以在使用select for update時,where 子句一定要帶上索引,否則極容易造成效能問題。
行鎖又細分三種實現演算法:
-
record lock:專門對索引項加鎖;
-
gap lock:間隙鎖,是對索引之間的間隙加鎖;
-
Next-key lock:是前面兩種的組合,對索引及其之間的間隙加鎖;
1.7頁面鎖
頁面鎖出現比較少,它的特點是開銷和加鎖時間界於表鎖和行鎖之間,會出現死鎖,鎖定粒度界於表鎖和行鎖之間,併發度一般。
二、死鎖
2.1死鎖原理
死鎖(Deadlock) 所謂死鎖:是指兩個或兩個以上的程序在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。由於資源佔用是互斥的,當某個程序提出申請資源後,使得有關程序在無外力協助下,永遠分配不到必需的資源而無法繼續執行,這就產生了一種特殊現象死鎖。
死鎖的四個必要條件:
-
互斥條件:一個資源每次只能被一個程序使用。
-
佔有且等待:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
-
不可強行佔有:程序已獲得的資源,在末使用完之前,不能強行剝奪。
-
迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。
2.2死鎖案例
案例一
首先建立一張訂單記錄表,用於做訂單的冪等性校驗防止重複生成訂單。
CREATE TABLE `order_record` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`order_no` int(11) DEFAULT NULL,
`status` int(4) DEFAULT NULL,
`create_date` datetime(0) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_order_status`(`order_no`,`status`) USING BTREE
) ENGINE = InnoDB
事務A | 事務B |
---|---|
關閉自動提交事務,set autocommit = 0; | set autocommit = 0; |
select id from order_record where order_no = 4 for update;//檢查是否存在訂單號為4的訂單 | |
select id from order_record where order_no = 5 for update;//檢查是否存在訂單號為5的訂單 | |
//如果沒有則插入資訊 insert into order_record(order_no,status,create_date) values(4,1,'2020-10-04 10:56:00'); 此時鎖等待中... |
|
//如果沒有則插入資訊 insert into order_record(order_no,status,create_date) values(5,1,'2020-10-04 10:56:00'); |
|
返回結果表明發生死鎖,ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | |
COMMIT;(未完成) | COMMIT;(未完成) |
分析:
由於order_no列為非唯一索引,而且此時是RR事務隔離級別,所以SELECT 的加鎖型別是gap lock,而且gap範圍是(4,+∞)。
當我們執行插入 SQL 時,會在插入間隙上再次獲取插入意向鎖。插入意向鎖其實也是一種 gap 鎖,它與 gap lock 是衝突的,事務 A 和事務 B 都持有間隙 (4,+∞)的 gap 鎖,而接下來的插入操作為了獲取到插入意向鎖,都在等待對方事務的 gap 鎖釋放,於是就造成了迴圈等待,導致死鎖。
案例二
InnoDB 儲存引擎的主鍵索引為聚簇索引,其它索引為輔助索引。如果兩個更新事務使用了不同的輔助索引,或者一個使用輔助索引,一個使用了聚簇索引,就都有可能導致鎖資源的迴圈等待,造成死鎖。
步驟:
首先,order_record表存在以下資料。
然後開啟兩個視窗
事務A | 事務B |
---|---|
BEGIN; | BEGIN; |
update order_record set status = 1 where order_no = 4; | |
mysql> update order_record set status = 1 where id = 4; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction//發生了死鎖 |
分析:
事務A | 事務B |
---|---|
首先獲取idx_order_status輔助索引 | |
獲取主鍵索引的行鎖 | |
根據輔助索引獲取主鍵索引,再獲取主鍵索引的行鎖 | |
更新status列時,需要idx_order_status輔助索引 |
所以再更新資料時,要儘量根據主鍵來更新,可以有效避免死鎖發生。
二、如何避免死鎖
通常有以下手段可以預防死鎖的發生:
- 在程式設計中儘量按照固定的順序來處理資料庫記錄,假設有兩個更新操作,分別更新兩條相同的記錄,但更新順序不一樣,有可能導致死鎖。
- 在允許幻讀和不可重複讀的情況下,儘量使用 RC 事務隔離級別,可以避免 gap lock 導致的死鎖問題。
- 更新表時,儘量使用主鍵更新。
- 避免長事務,儘量將長事務拆解,可以降低與其它事務發生衝突的概率;
- 設定鎖等待超時引數,我們可以通過 innodb_lock_wait_timeout 設定合理的等待超時閾值,特別是在一些高併發的業務中,我們可以儘量將該值設定得小一些,避免大量事務等待,佔用系統資源,造成嚴重的效能開銷。
如果真的發生了資料庫死鎖,也有以下方式處理:
-
檢視當前的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
-
檢視當前鎖定的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
-
檢視當前等鎖的事務
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-
殺死程序 kill pid
而且MySQL預設開啟了死鎖檢測機制,當檢測到死鎖後會選擇一個最小(鎖定資源最少的)的事務進行回滾。
三、總結
平常很少寫MySQL相關的文章,其實MySQL中的門道還是挺多的,本文關於間隙鎖等概念講的比較簡單,推薦部落格《mysql間隙鎖》。
以後可能會再寫一篇關於索引的,也有可能不會(主要是懶