1. 程式人生 > 實用技巧 >大話MySQL鎖

大話MySQL鎖

一、鎖介紹

不同儲存引擎支援的鎖是不同的,比如MyISAM只有表鎖,而InnoDB既支援表鎖又支援行鎖。

下圖展示了InnoDB不同鎖型別之間的關係:

圖中的概念比較多不好理解,下面依次進行說明。

1.1樂觀鎖

​ 樂觀鎖是相對悲觀鎖而言的,樂觀鎖假設資料一般情況下不會造成衝突,所在在資料進行提交更新時,才會對資料的衝突與否進行檢測,如果發現衝突了,則返回給使用者錯誤資訊,讓使用者決定如何處理,其核心是基於CAS演算法。樂觀鎖適用於讀多寫少的場景,可以提高程式吞吐量。

​ Mysql自帶的是沒有樂觀鎖的,但是可以通過表上加個version欄位來實現自己樂觀鎖。

假如要更新一個使用者的年齡,可以這樣做:

  1. 查出使用者id等於3的使用者資訊,select id,name,age,version from user where id = 3,得到如下的資料。
id Name Age Version
3 張三 26 1
  1. 更新張三的年齡為27,注意where條件帶上版本號。update user set age = 27,version = 2 where id = 3 and version = 1;

  2. 如果更新的結果是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) 所謂死鎖:是指兩個或兩個以上的程序在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的程序稱為死鎖程序。由於資源佔用是互斥的,當某個程序提出申請資源後,使得有關程序在無外力協助下,永遠分配不到必需的資源而無法繼續執行,這就產生了一種特殊現象死鎖。

死鎖的四個必要條件:

  1. 互斥條件:一個資源每次只能被一個程序使用。

  2. 佔有且等待:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。

  3. 不可強行佔有:程序已獲得的資源,在末使用完之前,不能強行剝奪。

  4. 迴圈等待條件:若干程序之間形成一種頭尾相接的迴圈等待資源關係。

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輔助索引

所以再更新資料時,要儘量根據主鍵來更新,可以有效避免死鎖發生。

二、如何避免死鎖

通常有以下手段可以預防死鎖的發生:

  1. 在程式設計中儘量按照固定的順序來處理資料庫記錄,假設有兩個更新操作,分別更新兩條相同的記錄,但更新順序不一樣,有可能導致死鎖。
  2. 在允許幻讀和不可重複讀的情況下,儘量使用 RC 事務隔離級別,可以避免 gap lock 導致的死鎖問題。
  3. 更新表時,儘量使用主鍵更新。
  4. 避免長事務,儘量將長事務拆解,可以降低與其它事務發生衝突的概率;
  5. 設定鎖等待超時引數,我們可以通過 innodb_lock_wait_timeout 設定合理的等待超時閾值,特別是在一些高併發的業務中,我們可以儘量將該值設定得小一些,避免大量事務等待,佔用系統資源,造成嚴重的效能開銷。

如果真的發生了資料庫死鎖,也有以下方式處理:

  1. 檢視當前的事務

    SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
    
  2. 檢視當前鎖定的事務

    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
    
  3. 檢視當前等鎖的事務

    SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
    
  4. 殺死程序 kill pid

而且MySQL預設開啟了死鎖檢測機制,當檢測到死鎖後會選擇一個最小(鎖定資源最少的)的事務進行回滾。

三、總結

平常很少寫MySQL相關的文章,其實MySQL中的門道還是挺多的,本文關於間隙鎖等概念講的比較簡單,推薦部落格《mysql間隙鎖》。
以後可能會再寫一篇關於索引的,也有可能不會(主要是懶