我們在高併發的場景下,經常會在異常日誌中看到“dead lock(死鎖)”的錯誤資訊。想了無數的解決方案,都沒有能夠最終的解決,到底是什麼原因引起了死鎖呢?要解決這個問題,我們就必須先了解透徹資料庫都有哪些鎖?他們的工作機制是什麼樣的。

那,讓我們開啟今天的學習之路吧。

為什麼資料庫要加鎖?

繼續深入資料庫 瞭解一下資料庫的鎖機制

當多條請求併發訪問一個數據庫資源時,有可能就會導致資料的不一致,因此,就需要一種機制來將資料庫的訪問順序化,從而保證資料庫資料的一致性,這個我們在

《資料庫常用的事務隔離級別都有哪些?都是什麼原理?》也有講到。事務就是就是一種順序化的機制,而事務要達到目的,就必須要有所的支援。

繼續深入資料庫 瞭解一下資料庫的鎖機制

資料庫都有哪些鎖?

由於資料庫的種類也不少,每種資料庫的鎖大致都相同,但是細節上略有不同,因此,我們就選擇MySql/InnoDB作為講解的物件。

InnoDB按照鎖的型別來劃分,主要分為了三個大類:共享鎖(Shared lock)、排它鎖,也叫獨佔鎖(Exclusive Locks)、意向鎖(Intent Locks)。其中,意向鎖又分為了意向共享和意向排他,因此,嚴格意義上來說,是有四種分類的鎖,分別是:

  1. 共享鎖(Shared lock),簡稱:S鎖
  2. 排它鎖(Exclusive Locks),簡稱:X鎖
  3. 意向共享鎖(Intent Shared lock),簡稱:IS鎖
  4. 意向排他鎖(Intent Exclusive Locks),簡稱:IX鎖

接下來,我們就聊聊這些鎖都是怎麼工作的。

繼續深入資料庫 瞭解一下資料庫的鎖機制

共享鎖

共享鎖,顧名思義,就是我雖然鎖住了這個資源,但是我並不會獨佔它,我同樣允許其他人使用這個資源。

繼續深入資料庫 瞭解一下資料庫的鎖機制

通常情況下,查詢就是使用的共享鎖。

例如:

事務A先執行了一個查詢

  1. select * from table; 

事務A還沒有執行完,事務B就執行了另一個查詢

  1. select * from table where id = 1; 

這個時候,事務B是可以優先於事務A完成他的查詢的,並不存在必須要事務A結束,才執行事務B的情況,這就是共享鎖的作用。

排它鎖

排它鎖,又叫獨佔鎖,顧名思義,我鎖住了,這東西就是我一個人的,誰也別想看,別想碰。

通常情況下,修改操作就是使用的排它鎖。

繼續深入資料庫 瞭解一下資料庫的鎖機制

例如:

事務A先執行了一個修改操作

  1. update table set Status = 1; 

事務B還事務A沒有完成時,執行了另一個修改操作

  1. update table set Status = 0 where id =1; 

這個時候,事務B就只有等著,到事務A執行完成以後,事務B才能夠繼續,這就是排它鎖的作用。

意向鎖

共享鎖、排它鎖按照其作用的粒度,可以鎖到行級,也可以鎖到頁級或表級。不過意向鎖只作用於表級,主要是用來標記一個事務對於這張表操作的一個意向。

例如:我有一個事務需要使用表鎖,那我就需要知道,這個表是否存在其他的鎖,如果有,可能我就需要等待。但是,我如果要排除其他鎖,我就需要一個一個記錄的遍歷,才知道是不是存在行鎖。因此,資料庫對於行鎖就提出另一個機制,就是意向鎖,如果你要對這個表進行行鎖時,那麼先在表上加一個意向鎖,方便其他事務查詢。

繼續深入資料庫 瞭解一下資料庫的鎖機制

因此,意向鎖就有了以下協議:

  • 一個事務獲得表t中某行的S鎖之前,必須先獲得t表上的IS鎖或者更強型別的鎖。
  • 一個事務獲得表t中某行的X鎖之前, 必須先獲得t表上的IX鎖。

現在我們知道了所的型別,接下來我們說說鎖的級別。

繼續深入資料庫 瞭解一下資料庫的鎖機制

根據鎖的顆粒或者級別不同,我們又把所分為了三個級別:表鎖(table-level locking)、頁鎖(page-level locking)、行鎖(row-level locking)。

MyISAM和MEMORY儲存引擎採用的是表鎖(table-level locking);BDB儲存引擎採用的是頁鎖(page-level locking),但也支援表鎖;InnoDB儲存引擎既支援行鎖(row-level locking),也支援表鎖,但預設情況下是採用行鎖。

而行鎖又包括了三種行鎖的演算法,分別是:

  1. 記錄鎖(Record Lock)
  2. 間隙鎖(Gap Lock)
  3. 臨鍵鎖(Next-Key Lock)

這裡有個小知識點:InnoDB的行鎖只針對索引項使用,也就是說,只有在通過索引檢索資料時,InnoDB才使用行鎖,其他時候都是使用的表鎖。

記錄鎖(Record Locks)

記錄鎖,顧名思義,就是鎖住一條記錄。這是Read Committed(讀提交)事務級別的預設鎖級別。

記錄鎖是作用於索引的,所以,當查詢不是作用於索引上時,系統會建立一個隱式的聚集索引,然後作用在索引上。

例如:

  1. select * from table where id = 1 lock in share mode; 

就是一個共享記錄鎖,

  1. select * from table for update where id = 1 ; 

就是一個排他記錄鎖。

間隙鎖(Gap Lock)

間隙鎖,它不會去鎖住索引本身,但是會鎖住的是一個索引的範圍。啟用它有一個前置條件,就是資料庫隔離級別必須是Repeatable Read(可重複讀),這也是InnoDB的預設隔離級別,假設我們將隔離級別降到Read Committed(讀提交),間隙鎖將會自動失效。

間隙鎖的使用,能夠有效的防止幻讀。

例如:

如果事務A執行了

  1. select * from table where id between 8 and 15 for update; 

這是,事務B想在事務A執行期間執行插入一條id是10的記錄,就會被阻止。因為這會導致事務A中的多次查詢資料不一致。

臨鍵鎖(Next-Key Lock)

臨鍵鎖就是記錄鎖+間隙鎖的組合方式。這是Repeatable Read(可重複讀)隔離級別的預設鎖級別。使用臨鍵鎖有一個好處,就是,假設我們執行執行一個查詢

  1. select * from table where id = 100; 

如果id是唯一索引,那麼臨鍵鎖就會降級為記錄鎖,鎖住這條記錄,而不是去鎖住一個範圍。

我們講完了這些鎖,那麼就不禁要問了,死鎖是怎麼產生的呢?

這就要說到另一個情況,就是鎖的升級。

鎖的升級

繼續深入資料庫 瞭解一下資料庫的鎖機制

假設,我們先進行了一個查詢,找到了目標資料,然後進行修改,在這個事務中,其實不同的階段,鎖的型別是不同的。

當我們進行查詢的時候,我們想資料庫先獲得了一個共享鎖,當我們要對這條資料進行更新的時候,並不是釋放共享鎖,然後再獲取排它鎖,而是進行了一個鎖的升級操作,直接將共享鎖升級成為了排它鎖。

而就是因為這個操作,可能導致了死鎖。

死鎖

繼續深入資料庫 瞭解一下資料庫的鎖機制

假設:事務A中有一個鎖升級操作,也就是先執行

  1. select * from table where id =1 

再執行

  1. update table set Status = 1 where id =1 

事務B中,同樣存在這樣的情況,先執行

  1. select * from table where id =1 

再執行

  1. update table set Name = '牛' where id =1 

而執行的順序恰好是:

  1. 事務A獲得了共享鎖,執行查詢;
  2. 事務B獲得了共享鎖,執行查詢;
  3. 事務A需要升級排它鎖,執行修改;
  4. 事務B也需要升級排它鎖,不能釋放共享鎖。

於是,死鎖就發生了。當然,還有一種交叉死鎖的情況,更為常見,大家可以自己百度看看了。

死鎖發生時,資料庫並不會直接檢查到死鎖的存在,只有在鎖等待超時的時候被發現,然後殺死其中一個請求。如果併發量高時,死鎖就會引起大量的執行緒掛起,佔用大量資源。

怎麼預防死鎖呢?

最直接的辦法就是,別在update前先select一次。

但是,這種情況在所難免,很多時候我們update時,都是需要先select一次的,如果所有地方要求不能select,那程式碼難度勢必就幾何級的上升。

還有一種辦法就是,加硬體,提高併發能力,這樣,出現兩次事務同時請求的概率就下降了。不過,這個方式成本太高。

當然,我們還可以直接在查詢時,就申請到最終事務需要的鎖級別,避免升級鎖的出現,也可以預防死鎖,例如查詢時就直接寫

  1. select * from table for update;