強人鎖男,MySQL到底有多少鎖?

前言

讀鎖寫鎖意向鎖,表鎖行鎖頁面鎖。

在學習Java併發編程的時候,肯定少不了學習。最常見的就是synchronized,鎖的概念不是很好理解,有的地方說是鎖住了一段代碼,有的地方說是鎖住了一個對象。弄得初學者都是丈二和尚——摸不着頭腦。

拋開這些結論性的說法,說一下我對鎖的理解(不管是Java中的鎖還是數據庫中的鎖,還是分佈式鎖)。當我們需要限制某段程序在同一時刻,最多能被1個線程同時執行的時候就需要鎖。這個幸運的線程怎麼選出來呢?那就讓他們去搶一個許可證吧,重點是需要保證這個許可證是唯一的,一次最多隻能被一個線程搶到。許可證不要拘泥於是this,還有可能是數據庫設置了唯一鍵的列、緩存中唯一的key或者一個文件目錄。只有搶到了這個許可證的線程,纔可以執行這段代碼,執行完成或者異常退出後自動釋放許可證

理解了這點,再說鎖住的是一段代碼、一個對象,甚至是一個線程都無所謂了,因爲鎖的作用就是在某一段時間內將一段代碼、一個對象(許可證)、一個線程綁定在一起。

MySQL中的鎖與存儲引擎有關,MyISAM只支持表級鎖,InnoDB既支持表級鎖,又支持行級鎖

InnoDB中的鎖

InnoDB實現了行級鎖,可以分爲兩種類型:共享(S)鎖定和排他(X)鎖定。

  • 共享(S)鎖允許持有該鎖的事務讀取一行,所有又叫讀鎖
  • 排他(X)鎖允許持有該鎖的事務更新或刪除行,所以又叫寫鎖

多個事務併發執行時,如果事務T1在某一行r上持有共享(S)鎖,那麼事務T2的對這行r的鎖請求將按以下方式處理:

  • T2對S鎖的請求可以立即獲得批准。T1和T2都在r上保持了S鎖
  • T2對X鎖的請求不能獲取批准。

如果事務T1在某一行r上擁有排他(X)鎖,則事務T2不能獲取鎖(不論是S鎖還是X鎖

換言之,S鎖和S鎖是兼容的,S鎖和X鎖是衝突的,X鎖和X鎖是衝突的

S鎖 X鎖
S鎖 衝突 衝突
X鎖 衝突 衝突

但是共享鎖排它鎖並不是指具體的兩種鎖,而是指兩類鎖。
同樣的樂觀鎖悲觀鎖也不是指具體的鎖,而是指兩類鎖。樂觀鎖是樂觀的認爲每次都不會發生衝突,只會在更新的時候檢查要更新的值有沒有被別人修改過,因爲沒有加鎖,所以樂觀鎖又叫無鎖。在數據庫中一般是用MVCC實現樂觀鎖,在Java中用CAS實現樂觀鎖。至於悲觀鎖就是悲觀的認爲每次都會發生衝突,所以每次修改都需要加鎖。

表鎖

表鎖是MySQL中粒度最大的一種鎖,簡單粗暴的鎖住整張表,實現簡單所以支持的併發度低。InnoDB和MyIASM都支持表鎖,表鎖分爲共享(S)鎖排他(X)鎖
表鎖的特點是實現簡單,併發度低。加鎖快,開銷小。不會出現死鎖。

行鎖

行鎖是MySQL中粒度最細的一種鎖,每次只鎖住要操作的那一行。實現複雜,支持的併發度高。只有InnoDB支持行鎖。行鎖也分爲共享(S)鎖排他(X)鎖。行鎖是對索引的鎖定。例如SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE,防止任何其他事務插入,更新或刪除t.c1值爲10的行。行鎖始終鎖定索引,對於沒有創建索引的表,InnoDB創建一個隱藏的聚簇索引並將該索引用於行鎖。
行鎖的特點是實現複雜,併發度高。加鎖慢,開銷大。會出現死鎖。

意向鎖(Intention Locks)

InnoDB支持多種粒度鎖定,允許行鎖表鎖並存。爲了使在多個粒度級別上的鎖定變得切實可行,InnoDB實現了意圖鎖意向鎖是表級鎖,表示事務稍後對錶中的行需要上哪種類型的鎖(共享鎖排他鎖)。有兩種類型的意圖鎖:
* 意向共享鎖(IS)表示事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的 IS 鎖。
* 意向排他鎖(IX)表示事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的 IX 鎖。

SELECT ... LOCK IN SHARE MODE設置IS鎖定,而SELECT ... FOR UPDATE設置IX鎖定
表級鎖之間的兼容性如下

X鎖 IX鎖 S鎖 IS鎖
X鎖 衝突 衝突 衝突 衝突
IX鎖 衝突 兼容 衝突 兼容
S鎖 衝突 衝突 兼容 兼容
IS鎖 衝突 兼容 兼容 兼容

有的同學一看到這麼多種情況就頭暈,死記硬背是不可能死記硬背的,這輩子都不會死記硬背。既然這樣,那就乾脆不記,花點時間深入瞭解一下爲什麼要設計成表中這樣。

要想理解這個表,首先得理解爲什麼要有意向鎖(Intention Locks),關於意向鎖的作用,官方文檔上給出了這麼一句話:

Intention locks do not block anything except full table requests (for example, LOCK TABLES … WRITE). The main purpose of intention locks is to show that someone is locking a row, or going to lock a row in the table.

意向鎖不會阻止任何其他請求,除了(鎖定)全表請求(例如LOCK TABLES ... WRITE)外。意向鎖定的主要目的是:聲明已經有事務正在鎖定表中的行,或者即將鎖定表中的行。

這句話透露出了意向鎖雖然是表鎖,但是和行鎖是完全兼容的(包括共享鎖排它鎖)。但是聲明表中的行正在被鎖定或者即將被鎖定,有什麼用呢?

假設事務A給表中某一行加了排它鎖,而事務B想給這個表加一個全表的排他鎖,這時候事務B就需要判斷當前表中到底有沒有排他鎖,如果有的話,是不能成功加上去表的排它鎖的。此時,如果沒有意向鎖,事務B只能遍歷整個索引去判斷,這樣無疑是低效的。爲了解決這個問題,MySQL引入了意向鎖。當事務A給某一行加了排它鎖或者共享鎖後,會分別在表上加意向排它鎖(IX)或者意向共享鎖(IS),這時,事務B就可很輕鬆的判斷當前表是否有行鎖,這也就是前文所說的:爲了使在多個粒度級別上的鎖定變得切實可行,InnoDB實現了意圖鎖

理解了意圖鎖的作用,再來看看上面的表格。S鎖和X鎖之間的兼容性前文已經理清了,還剩下IX和IS之間以及S/X和IS/IX的兼容性。

由於某一行被加了S鎖或者X鎖後,表上都會加上對應的IS鎖和IX鎖。另一個事務想鎖住另一條記錄,也得加上對應的IS鎖或者IX鎖,所以IS和IS、IS和IX以及IX和IX必定是兼容的,不然整個表最多隻能上一個行鎖。

至於S鎖、X鎖和IS鎖、IX鎖的兼容性則需與分情況討論了。當整個表上了X鎖之後,再也不能上別的X鎖或者S鎖了,所以X鎖和IS、IX都是衝突的。當整個表上了S鎖之後,不能再上X鎖了,但是還可以上S鎖,所以S鎖和IX鎖是衝突的,但是和IS鎖是兼容的。

這樣理解了之後,再看上面的那個表格,似乎也變得有規律了。

間隙鎖(Gap Locks)

間隙鎖鎖定的是索引記錄之間的間隙,或者在第一個或最後一個索引記錄之前的間隙。例如SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE,可以防止其他事務將t.c1爲15的記錄插入表中,因爲這兩個值之間的間隙是被鎖定的。
間隙可能跨越單個索引值,多個索引值,甚至爲空。
間隙鎖的唯一目的是防止其他事務在間隙中插入數據。間隙鎖可以共存。一個事務執行的間隙鎖,不會阻止另一事務對相同的間隙進行間隙鎖定。

Next-Key Locks

Next-Key Locks是索引記錄上的行鎖索引記錄之前的間隙上的間隙鎖的組合。如果一個session在索引中的記錄R上具有共享排他鎖,則另一session不能按照索引順序在R之前的間隙中插入新的索引記錄。
默認情況下,InnoDB設置的事務隔離級別是REPEATABLE READ。在這種情況下,InnoDB使用Next-Key Locks進行搜索和索引掃描,這可以防止幻讀(虛讀)。關於MySQL隔離級別相關的問題,請參考:面試官:MySQL事務是怎麼實現的

插入意圖鎖(Insert Intention Locks)

插入意圖鎖是在行插入之前,通過INSERT操作設置的間隙鎖的一種類型。如果多個事務想要在同一個間隙中插入不同的值(也就是插入的位置不同),則這多個事務均不會被阻塞。假設有索引記錄,其值分別爲4和7。單獨的事務分別嘗試插入值5和6,在獲得插入行的排他鎖之前,每個事務都使用插入意圖鎖來鎖定4和7之間的間隙,但不會互相阻塞,因爲插入的行是無衝突的。

自增鎖(AUTO-INC Locks)

AUTO-INC鎖是一種特殊的表級鎖,由事務插入具有AUTO_INCREMENT列的表中獲得。在最簡單的情況下,如果一個事務正在向表中插入值,則任何其他事務都必須等待這個事務在該表中進行插入,以便第一個事務插入的行接收連續的主鍵值。

MyISAM中的鎖

相比之下MyISAM中的鎖就簡單多了,因爲MyISAM只支持表鎖。並且共享鎖排它鎖也滿足如下關係

S鎖 X鎖
S鎖 衝突 衝突
X鎖 衝突 衝突

死鎖

前文提到InnoDB行鎖可能是出現死鎖,死鎖是一個計算機領域的概念,而不是數據庫特有的,所以死鎖的概念是通用的。不太瞭解死鎖的同學請參考:面試官:請手寫一段必然死鎖的代碼。這裏演示下MySQL行鎖導致的死鎖,這也是官網上給出的例子
首先準備數據

## 創建表
CREATE TABLE t (i INT) ENGINE = InnoDB;

## 新增數據
INSERT INTO t (i) VALUES(1);

具體操作的時間線如下

事務A 事務B
T1 START TRANSACTION
T2 SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE
T3 START TRANSACTION
T4 DELETE FROM t WHERE i = 1
T5 DELETE FROM t WHERE i = 1

事務A在T5執行時,事務B會收到一條錯誤信息

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

此處發生死鎖,因爲事務A需要X鎖才能刪除該行。但是,不能授予該鎖定請求,因爲事務B已經具有X鎖定請求,並且正在等待事務A釋放其S鎖定。由於B事先要求X鎖,因此A持有的S鎖也不能升級爲X鎖。結果,InnoDB爲其中一個客戶端生成一個錯誤並釋放其鎖。此時時,可以授予對另一個客戶端的鎖定請求,並從表中刪除該行。

也就是說MySQL可以自動檢測死鎖,並且放棄一個事務來成全另一個事務,這點與Java程序中的死鎖不一樣。

查詢事務、鎖相關的參考命令如下:

## 查看當前事務狀態
select trx_id, trx_state, trx_started, trx_requested_lock_id, trx_wait_started, trx_query, trx_isolation_level from information_schema.innodb_trx;

## 查看當前鎖定的事務
select * from information_schema.innodb_locks;

## 查看當前正在等待鎖的事務
select * from information_schema.innodb_lock_waits;

總結

鎖是MySQL非常重要的一個部分,雖然一般情況下鎖的鎖定和釋放都由MySQL自動完成。但是瞭解MySQL中的鎖還是很有必要,它讓我們進一步的瞭解了MySQL是如何處理併發的。

參考

  • https://dev.mysql.com/doc/refman/5.6/en/innodb-locking.html
  • https://zhuanlan.zhihu.com/p/29150809
  • https://www.zhihu.com/question/51513268/answer/127777478
  • https://dev.mysql.com/doc/refman/5.6/en/innodb-deadlocks.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章