MySQL鎖機制——你想知道的都在這了!

一、鎖的類型

1. 行鎖

(1)共享鎖(S Lock)允許事務讀一行數據

(2)排它鎖 (X Lock) 允許事務讀一行數據

image.png

2. 表鎖(意向鎖)

鎖定允許事務在行級上的鎖和表級上的鎖同時存在。爲了支持在不同粒度上進行加鎖操作,InnoDB存儲引擎支持一種額外的鎖方式

(1)意向共享鎖(IS Lock)事務想要獲得一張表中某幾行的共享鎖

(2)意向排他鎖(IX Lock)事務想要獲得一張表中某幾行的排他鎖

由於InnoDB存儲引擎支持的是行級別的鎖,因此意向鎖其實不會阻塞除全表掃以外的任何請求。故表級意向鎖與行級鎖的兼容性如下所示

image.png

若將上鎖的對象看成一棵樹,那麼對最下層的對象上鎖,也就是對最細粒度的對象進行上鎖,那麼首先需要對粗粒度的對象上鎖。例上圖,如果需要對頁上的記錄r進行上X鎖,那麼分別需要對數據庫A、表、頁上意向鎖IX,最後對記錄r上X鎖。若其中任何一個部分導致等待,那麼該操作需要等待粗粒度鎖的完成。舉例來說,在對記錄r加X鎖之前,已經有事務對錶1進行了S表鎖,那麼表1上已存在S鎖,之後事務需要對記錄r在表1上加上IX,由於不兼容,所以該事務需要等待表鎖操作的完成。

3. 意向鎖到底有什麼作用?

innodb的意向鎖主要用戶多粒度的鎖並存的情況。比如事務A要在一個表上加S鎖,如果表中的一行已被事務B加了X鎖,那麼該鎖的申請也應被阻塞。如果表中的數據很多,逐行檢查鎖標誌的開銷將很大,系統的性能將會受到影響。爲了解決這個問題,可以在表級上引入新的鎖類型來表示其所屬行的加鎖情況,這就引出了“意向鎖”的概念。

舉個例子,如果表中記錄1億,事務A把其中有幾條記錄上了行鎖了,這時事務B需要給這個表加表級鎖,如果沒有意向鎖的話,那就要去表中查找這一億條記錄是否上鎖了。如果存在意向鎖,那麼假如事務A在更新一條記錄之前,先加意向鎖,再加X鎖,事務B先檢查該表上是否存在意向鎖,存在的意向鎖是否與自己準備加的鎖衝突,如果有衝突,則等待直到事務A釋放,而無須逐條記錄去檢測。事務B更新表時,其實無須知道到底哪一行被鎖了,它只要知道反正有一行被鎖了就行了。主要作用是處理行鎖和表鎖之間的矛盾,能夠顯示“某個事務正在某一行上持有了鎖,或者準備去持有鎖”

二、鎖的算法

1. Record Lock:單個行記錄上的鎖

2. Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄本身

3. Next-Key Lock∶Gap Lock+Record Lock,鎖定一個範圍、索引之間的間隙,並且鎖定記錄本身;目的是爲了防止幻讀

三、mysql如何做到讀寫並行(多版本控制)?

多版本併發控制 MVCC,是行級鎖的一個變種,通過保存數據在某個時間節點的快照(snapshot),類似實現了行級鎖。由此不同事務對同一表,同一時刻看到的數據可能是不一樣的。 實現上通過在不同的數據行後增加創建日期版本號和刪除日期版本號,且版本號不斷遞增,進而實現了數據快照

1. 讀的類型

(1)一致性非鎖定讀(快照讀)

在事務隔離級別提交讀(RC)和可重複讀(RR)下,InnoDB存儲引擎使用非鎖定的一致性讀

① RC模式下,讀取最新的快照

② RR模式下,讀取事務開始時的快照

(2)一致性鎖定讀 (當前讀)

① 隔離級別爲未提交讀(RN)時讀取都是當前讀

② SELECT…FOR UPDATE (加寫鎖)

③ SELECT…LOCK IN SHARE MODE (加讀鎖)

四、加鎖處理分析

下面兩條簡單的SQL,他們加什麼鎖?

select * from t1 where id = 10 delete from t1 where id = 10 如果要分析加鎖情況,必須還要知道以下的一些前提,前提不同,加鎖處理的方式也不同

(1)前提一:id列是不是主鍵?

(2)前提二:當前系統的隔離級別是什麼?

(3)前提三:id列如果不是主鍵,那麼id列上有索引嗎?

(4)前提四:id列上如果有二級索引,那麼這個索引是唯一索引嗎?

(5)前提五:兩個SQL的執行計劃是什麼?索引掃描?全表掃描?

根據上述情況,有以下幾種組合:

① 組合一:id列是主鍵,RC隔離級別

② 組合二:id列是二級唯一索引,RC隔離級別

③ 組合三:id列是二級非唯一索引,RC隔離級別

④ 組合四:id列上沒有索引,RC隔離級別

⑤ 組合五:id列是主鍵,RR隔離級別

⑥ 組合六:id列是二級唯一索引,RR隔離級別

⑦ 組合七:id列是二級非唯一索引,RR隔離級別

⑧ 組合八:id列上沒有索引,RR隔離級別

⑨ 組合九:Serializable隔離級別

排列組合還沒有列舉完全,但是看起來,已經很多了。真的有必要這麼複雜嗎?事實上,要分析加鎖,就是需要這麼複雜。但是從另一個角度來說,只要你選定了一種組合,SQL需要加哪些鎖,其實也就確定了。接下來挑幾個比較經典的組合

1. 組合一:id主鍵+RC

這個組合,是最簡單,最容易分析的組合。id是主鍵,Read Committed隔離級別,給定SQL:delete from t1 where id = 10; 只需要將主鍵上,id = 10的記錄加上X鎖即可。如下圖所示:

image.png

結論:id是主鍵時,此SQL只需要在id=10這條記錄上加X鎖即可。

2. 組合二:id唯一索引+RC

這個組合,id不是主鍵,而是一個Unique的二級索引鍵值。那麼在RC隔離級別下,delete from t1 where id = 10; 需要加什麼鎖呢?見下圖:

image.png

id是unique索引,而主鍵是name列。此時,加鎖的情況由於組合一有所不同。由於id是unique索引,因此delete語句會選擇走id列的索引進行where條件的過濾,在找到id=10的記錄後,首先會將unique索引上的id=10索引記錄加上X鎖,同時,會根據讀取到的name列,回主鍵索引(聚簇索引),然後將聚簇索引上的name = ‘d’ 對應的主鍵索引項加X鎖。

結論:若id列是unique列,其上有unique索引。那麼SQL需要加兩個X鎖,一個對應於id unique索引上的id = 10的記錄,另一把鎖對應於聚簇索引上的[name='d',id=10]的記錄、

3. 組合三:id非唯一索引+RC

相對於組合一、二,組合三又發生了變化,隔離級別仍舊是RC不變,但是id列上的約束又降低了,id列不再唯一,只有一個普通的索引。假設delete from t1 where id = 10; 語句,仍舊選擇id列上的索引進行過濾where條件,那麼此時會持有哪些鎖?同樣見下圖:

image.png

根據此圖,可以看到,首先,id列索引上,滿足id = 10查詢條件的記錄,均已加鎖。同時,這些記錄對應的主鍵索引上的記錄也都加上了鎖。與組合二唯一的區別在於,組合二最多隻有一個滿足等值查詢的記錄,而組合三會將所有滿足查詢條件的記錄都加鎖。

結論:若id列上有非唯一索引,那麼對應的所有滿足SQL查詢條件的記錄,都會被加鎖。同時,這些記錄在主鍵索引上的記錄,也會被加鎖。

4. 組合四:id無索引+RC

相對於前面三個組合,這是一個比較特殊的情況。id列上沒有索引,where id = 10;這個過濾條件,沒法通過索引進行過濾,那麼只能走全表掃描做過濾。對應於這個組合,SQL會加什麼鎖?或者是換句話說,全表掃描時,會加什麼鎖?這個答案也有很多:有人說會在表上加X鎖;有人說會將聚簇索引上,選擇出來的id = 10;的記錄加上X鎖。那麼實際情況呢?請看下圖:

image.png

由於id列上沒有索引,因此只能走聚簇索引,進行全部掃描。從圖中可以看到,滿足刪除條件的記錄有兩條,但是,聚簇索引上所有的記錄,都被加上了X鎖。無論記錄是否滿足條件,全部被加上X鎖。既不是加表鎖,也不是在滿足條件的記錄上加行鎖。

有人可能會問?爲什麼不是隻在滿足條件的記錄上加鎖呢?這是由於MySQL的實現決定的。如果一個條件無法通過索引快速過濾,那麼存儲引擎層面就會將所有記錄加鎖後返回,然後由MySQL Server層進行過濾。因此也就把所有的記錄,都鎖上了。

結論:若id列上沒有索引,SQL會走聚簇索引的全掃描進行過濾,由於過濾是由MySQL Server層面進行的。因此每條記錄,無論是否滿足條件,都會被加上X鎖。但是,爲了效率考量,MySQL做了優化,對於不滿足條件的記錄,會在判斷後放鎖,最終持有的,是滿足條件的記錄上的鎖,但是不滿足條件的記錄上的加鎖/放鎖動作不會省略。同時,優化也違背了2PL的約束。

5. 組合五:id主鍵+RR

上面的四個組合,都是在Read Committed隔離級別下的加鎖行爲,接下來的四個組合,是在Repeatable Read隔離級別下的加鎖行爲。

組合五,id列是主鍵列,Repeatable Read隔離級別,針對delete from t1 where id = 10; 這條SQL,加鎖與組合一:[id主鍵,Read Committed]一致。

6. 組合六:id唯一索引+RR

與組合五類似,組合六的加鎖,與組合二:[id唯一索引,Read Committed]一致。兩個X鎖,id唯一索引滿足條件的記錄上一個,對應的聚簇索引上的記錄一個。

7. 組合七:id非唯一索引+RR

還記得前面提到的MySQL的四種隔離級別的區別嗎?RC隔離級別允許幻讀,而RR隔離級別,不允許存在幻讀。但是在組合五、組合六中,加鎖行爲又是與RC下的加鎖行爲完全一致。那麼RR隔離級別下,

組合七,Repeatable Read隔離級別,id上有一個非唯一索引,執行delete from t1 where id = 10; 假設選擇id列上的索引進行條件過濾,最後的加鎖行爲,是怎麼樣的呢?同樣看下面這幅圖:  

image.png

結論:Repeatable Read隔離級別下,id列上有一個非唯一索引,對應SQL:delete from t1 where id = 10; 首先,通過id索引定位到第一條滿足查詢條件的記錄,加記錄上的X鎖,加GAP上的GAP鎖,然後加主鍵聚簇索引上的記錄X鎖,然後返回;然後讀取下一條,重複進行。直至進行到第一條不滿足條件的記錄[11,f],此時,不需要加記錄X鎖,但是仍舊需要加GAP鎖,最後返回結束。什麼時候會取得gap lock或nextkey lock  這和隔離級別有關,只在REPEATABLE READ或以上的隔離級別下的特定操作纔會取得gap lock或nextkey lock。

8. 組合八:id無索引+RR

組合八,Repeatable Read隔離級別下的最後一種情況,id列上沒有索引。此時SQL:delete from t1 where id = 10; 沒有其他的路徑可以選擇,只能進行全表掃描。最終的加鎖情況,下圖所示:  

image.png

結論:在Repeatable Read隔離級別下,如果進行全表掃描的當前讀,那麼會鎖上表中的所有記錄,同時會鎖上聚簇索引內的所有GAP,杜絕所有的併發 更新/刪除/插入 操作。當然,也可以通過觸發semi-consistent read,來緩解加鎖開銷與併發影響,但是semi-consistent read本身也會帶來其他問題,不建議使用。

9. 組合九:Serializable

針對前面提到的簡單的SQL,最後一個情況:Serializable隔離級別。對於SQL2:delete from t1 where id = 10; 來說,Serializable隔離級別與Repeatable Read隔離級別完全一致,因此不做介紹。

Serializable隔離級別,影響的是SQL1:select * from t1 where id = 10; 這條SQL,在RC,RR隔離級別下,都是快照讀,不加鎖。但是在Serializable隔離級別,SQL1會加讀鎖,也就是說快照讀不復存在,MVCC併發控制降級爲Lock-Based CC。

結論:在MySQL/InnoDB中,所謂的讀不加鎖,並不適用於所有的情況,而是隔離級別相關的。Serializable隔離級別,讀不加鎖就不再成立,所有的讀操作,都是當前讀。

五、死鎖案例

1. 不同表相同記錄行鎖衝突

這種情況很好理解,事務A和事務B操作兩張表,但出現循環等待鎖情況。

image.png

2. 相同表記錄行鎖衝突

這種情況比較常見,之前遇到兩個job在執行數據批量更新時,jobA處理的的id列表爲[1,2,3,4],而job處理的id列表爲[8,9,10,4,2],這樣就造成了死鎖。

image.png

3. 不同索引鎖衝突

這種情況比較隱晦,事務A在執行時,除了在二級索引加鎖外,還會在聚簇索引上加鎖,在聚簇索引上加鎖的順序是[1,4,2,3,5],而事務B執行時,只在聚簇索引上加鎖,加鎖順序是[1,2,3,4,5],這樣就造成了死鎖的可能性。

image.png

4. gap鎖衝突

innodb在RR級別下,如下的情況也會產生死鎖,比較隱晦。不清楚的同學可以自行根據上節的gap鎖原理分析下。

image.png

六、如何儘可能避免死鎖

以固定的順序訪問表和行。比如對第2節兩個job批量更新的情形,簡單方法是對id列表先排序,後執行,這樣就避免了交叉等待鎖的情形;又比如對於3.1節的情形,將兩個事務的sql順序調整爲一致,也能避免死鎖。

大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。

在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖概率。

降低隔離級別。如果業務允許,將隔離級別調低也是較好的選擇,比如將隔離級別從RR調整爲RC,可以避免掉很多因爲gap鎖造成的死鎖。

爲表添加合理的索引。可以看到如果不走索引將會爲表的每一行記錄添加上鎖,死鎖的概率大大增大。

七、如何查看鎖?

從InnoDB1.0開始,在INFORMATION_SCHEMA架構下添加了表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS。(詳情見附錄)通過這三張表,用戶可以更簡單地監控當前事務並分析可能存在的鎖問題。

image.png

八、mysql是如何預防死鎖的?

1. innodb_lock_wait_timeout 等待鎖超時回滾事務

直觀方法是在兩個事務相互等待時,當一個等待時間超過設置的某一閥值時,對其中一個事務進行回滾,另一個事務就能繼續執行。

2. wait-for graph算法來主動進行死鎖檢測

每當加鎖請求無法立即滿足需要並進入等待時,wait-for graph算法都會被觸發。

wait-for graph要求數據庫保存以下兩種信息:

(1)鎖的信息鏈表

(2)事務等待鏈表

通過上述鏈表可以構造出一張圖,而在這個圖中若存在迴路,就代表存在死鎖,因此資源間相互發生等待。在wait-for graph中,事務爲圖中的節點。而在圖中,事務T1指向T2邊的定義爲:

事務T1等待事務T2所佔用的資源

事務T1最終等待T2所佔用的資源,也就是事務之間在等待相同的資源,而事務T1發生在事務T2的後面

image.png

示例事務狀態和鎖的信息

在Transaction Wait Lists中可以看到共有4個事務t1、t2、t3、t4,故在wait-for graph中應有4個節點。而事務t2對row1佔用x鎖,事務t1對row2佔用s鎖。事務t1需要等待事務t2中row1的資源,因此在wait-for graph中有條邊從節點t1指向節點t2。事務t2需要等待事務t1、t4所佔用的row2對象,故而存在節點t2到節點t1、t4的邊。同樣,存在節點t3到節點t1、t2、t4的邊,因此最終的wait-for graph如下圖所示。

image.png

ps:若存在則有死鎖,通常來說InnoDB存儲引擎選擇回滾undo量最小的事務並從新開始

九、附錄

1. INNODB_ROW_LOCK

1240

2. INNODB_TRX

提供有關當前正在內部執行的每個事務的信息 InnoDB,包括事務是否在等待鎖定,事務何時啓動以及事務正在執行的SQL語句(如果有)。

1240

1240

3. INNODB_LOCKS

提供有關InnoDB 事務已請求但尚未獲取的每個鎖的信息,以及事務持有的阻止另一個事務的每個鎖。

1240

4. INNODB_LOCK_WAITS

包含每個被阻止InnoDB 事務的一個或多個行,指示它已請求的鎖以及阻止該請求的任何鎖。

1240


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章