解析數據庫鎖協議和InnoDB鎖機制(全面解析行級鎖、表級鎖、排他鎖、共享鎖、悲觀鎖、樂觀鎖等常用鎖)

前言

數據庫通過鎖以及鎖協議來進行併發控制,解決併發事務帶來的問題,本篇博文主要是解析數據庫的鎖協議和Mysql的默認存儲引擎InnoDB的鎖機制。
如果對事務隔離級別以及併發事務帶來的問題不熟悉可以翻閱我的另外一篇博文–《解析事務隔離(事務隔離是如何解決髒讀、幻讀、不可重複讀等問題)》
這篇文章中會涉及一些MVCC以及快照讀、當前讀的概念,如果不是很瞭解可以翻閱我另外一篇關於MVCC在InnoDB中實現原理的博文–《InnoDB的MVCC實現原理(InnoDB如何實現MVCC以及MVCC的工作機制)》

鎖協議

在介紹鎖之前,我先介紹下鎖協議,爲了進行併發控制,數據庫的鎖協議主要有兩種,一種是封鎖協議,另外一個是兩段鎖協議;封鎖協議規定了何時加鎖和該加什麼鎖以及何時釋放鎖的規則,而兩段鎖協議除了規定做相應操作前加相應的鎖,更是嚴格的將事務的整個過程分成加鎖階段和解鎖階段兩個過程,加鎖階段不能進行解鎖,解鎖過程不能加鎖。

封鎖協議

封鎖協議規定了何時加鎖、釋放鎖的規則,不同的規則可用於實現不同的隔離級別,解決不同的併發事務問題。(X鎖即爲排他鎖,S鎖即爲共享鎖,對X鎖和S鎖不瞭解的可以滑動到下面先進行了解)

一級封鎖協議:更新數據前需要先加X鎖,直到事務結束才釋放X鎖,讀數據是不需要加S鎖。所以只能解決第一類更新丟失問題,不能解決髒讀和不可重複讀等問題。

二級封鎖協議:在一級封鎖的基礎上,事務在讀取數據之前必須先對其加上S鎖,讀完即可釋放S鎖。可以解決第一類更新丟失問題和髒讀。

三級封鎖協議:一級封鎖協議的基礎上,事務在讀取數據之前必須先對其加S鎖,直到事務結束才釋放。解決了丟失修改、髒讀和不可重複讀的問題。

三級封鎖協議能解決不可重複讀的原因

如下表中的情況,若什麼鎖都不用,會出現不可重複讀情況。若使用二級鎖協議,事務2在時間點2讀取完A值後就釋放S鎖,此時事務1可以獲得X鎖進行數據修改,時間點6,事務1提交事務,釋放X鎖,時間點7,事務2再次獲得S鎖讀取值,事務2出現不可重複讀問題,因此二級鎖協議不能防止不可重複讀問題。而若使用三級鎖協議,事務2在時間點2讀取完A值後並不釋放S鎖,一直到事務結束之後才釋放S鎖,事務1不可能在事務2讀數據的過程中獲得X鎖,因此事務2第二次讀A值還是30,防止了不可重複讀問題。三級封鎖協議通過規定了S鎖在事務結束才釋放禁止事務讀的過程中其它事務進行寫操作來防止了不可重複讀問題。

時間順序 事務1 事務2
1 開始事務
2 讀取A值30
3 開始事務
4 讀取A值30
5 A=A-10
6 提交事務
7 讀取A值20
8 提交事務

兩段鎖協議

兩段鎖協議除了規定做相應操作前加相應的鎖,更是嚴格的將事務的整個過程分成加鎖階段和解鎖階段兩個過程,加鎖階段不能進行解鎖,解鎖過程不能加鎖。

加鎖階段: 在該階段可以進行加鎖操作。在對任何數據進行讀操作之前要申請並獲得S鎖,在進行寫操作之前要申請並獲得X鎖。加鎖不成功,則事務進入等待狀態,直到加鎖成功才繼續執行。(加鎖階段沒有明確的時間規定,所有的加鎖操作做完,加鎖階段就可以結束了)
解鎖階段: 當事務釋放了一個封鎖以後,事務進入解鎖階段,在該階段只能進行解鎖操作不能再進行加鎖操作。

通過以下兩個事務過程進行兩段鎖的說明,例如事務T1:Lock A,Lock B,unLock B,LockC,unLock A,unLockC,T1事務這個過程不滿足兩段鎖協議,因爲加鎖階段對B進行了解鎖;事務T2:Lock A,Lock B,LockC,unLock A,unLockC,unLock B,T2這個過程是滿足兩段鎖協議,做完所有的加鎖操作之後再進行解鎖。注意兩段鎖協議的重點是事務內的所有加鎖操作都在解鎖操作之前。

那麼兩段鎖的意義在哪?兩段鎖協議爲什麼要有加鎖和解鎖分階段這種規定呢?理論上兩段鎖協議是用來實現可串行化調度,但其實並不見得,如果你仔細閱讀了博文上面三級封鎖協議,你就會發現三級封鎖協議其實就是基於兩段鎖協議的基礎上,三級封鎖協議無論是讀鎖還是寫鎖都是事務結束才釋放,因此它也就有了加鎖階段和解鎖階段,就連三級封鎖協議都不能處理幻讀,那何況兩段鎖協議呢?兩段鎖協議在我看來只是理論上實現了可串行化調度,其實並沒有(若有不同意的歡迎在評論區交流或者私信交流),它們都需要和相應的鎖算法一同使用才能實現真正的可串行化隔離級別,例如InnoDB使用的Next-Key Locks算法(下面有詳解)

鎖按照鎖粒度分類可分爲行級鎖、頁級鎖、表級鎖;按照實現思想來分可分爲悲觀鎖和樂觀鎖。雖然下圖中排他鎖和共享鎖都歸入了粒度分類中,但其實排他鎖和共享鎖都是悲觀鎖的不同實現,也可以歸入其子類。
在這裏插入圖片描述

表級鎖和行級鎖

表級鎖是對整張表進行上鎖,而行級鎖是對錶中的一行行的數據進行上鎖。Mysql的默認存儲引擎InnoDB支持表級鎖和行級鎖,默認使用行級鎖,能先使用行級鎖就先使用行級鎖,不能使用行級鎖就使用表級鎖。InnoDB中行級鎖是建立在表的索引上面,因此只有通過索引條件檢索數據才使用行級鎖,否則,InnoDB將使用表鎖。另外一個常用存儲引擎MyISAM只支持表級鎖。

頁級鎖是鎖定粒度介於行級鎖和表級鎖中間的一種鎖,即使你只需要一行數據,它也會把那行數據所在的數據頁整個鎖住。存儲引擎BDB採用頁面鎖或表級鎖,默認爲頁面鎖。因爲頁級鎖使用少,所以這裏不做過多描述。

比較

表級鎖:鎖粒度大、鎖住的範圍大、使用資源少、加鎖快、不會出現死鎖、鎖衝突概率高、併發程度低。

行級鎖:鎖粒度小、鎖住的範圍小、使用的資源多、加鎖慢、會出現死鎖、鎖衝突概率低、併發程度高。

表級鎖

表級鎖是鎖定粒度最大的一種鎖,對當前操作的整張表加鎖,鎖衝突概率高,併發程度低。表級鎖的併發程度低,那爲什麼不使用行級鎖而使用表級鎖?當你需要鎖住表中大多數的數據,使用表級鎖性價比更高,而且事務比較複雜時,使用行級鎖會出現死鎖,不如使用表級鎖。表級鎖主要有排他鎖(寫鎖)、共享鎖(讀鎖)、意向排他鎖、意向共享鎖等四種鎖。

排他鎖(X鎖)

排他鎖就是寫鎖,也叫做X鎖。當事務A給表加上排他鎖時,事務A可以對該表進行讀或者寫,在排他鎖被釋放前,其它事務不能對該表加上任意類型的鎖,即不允許其它事務讀寫該表數據。

共享鎖(S鎖)

共享鎖就是讀鎖,也叫做S鎖。當事務A給表加上共享鎖,事務A就只能對該表做讀操作,不能進行寫操作,其它事務都可以給表加上共享鎖,但不能加排他鎖。在表上的共享鎖全被釋放完畢之前,不能給表加上排他鎖。共享鎖的作用是可以讓多個事務同時讀表數據,但只要有一個事務讀表數據時,其它任何事務都不能來更新表數據。

X鎖和S鎖小結

X鎖主要是用來防止事務寫表時,其它併發事務來讀寫該表,造成第一類丟失修改、髒讀等問題。
S鎖主要是用來防止事務讀表時,其它併發事務寫該表,可以防止不可重複讀,因此可以用X鎖和S鎖一起來實現REPEATABLE-READ隔離級別。

我們在使用數據庫的實際過程中,往往沒有注意到鎖的使用,這是因爲數據庫使用的存儲引擎往往都是在進行操作時根據SQL語句自動加鎖,比如MyISAM存儲引擎只支持表級鎖,MyISAM在執行查詢語句SELECT前,會自動給涉及的所有表加讀鎖,在執行更新操作(如UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預。當然在某些特殊情況下,若需要用戶添加鎖,也可手動添加鎖。

意向鎖

意向鎖的作用主要是讓行級鎖和表級鎖共存,實現多粒度鎖機制。意向鎖分爲意向排他鎖(IX鎖)和意向共享鎖(IS鎖)兩種。

InnoDB支持行級鎖和表級鎖,默認是行級鎖,InnoDB有以下兩條規定:
(1)在事務獲取表中某行的行共享鎖之前,它必須首先獲取該表中的IS鎖或更強的鎖。
(2)在事務獲取表中某行的行排它鎖之前,它必須首先獲取該表中的IX鎖。

一個表中意向共享鎖和意向排他鎖可以同時並存多個,只加意向鎖不會導致鎖衝突,但意向鎖會與X鎖或者S鎖衝突,例如兩種意向鎖都會與X鎖衝突,IX鎖與S鎖衝突,當發生衝突時,會導致後加鎖的事務阻塞。

意向鎖存在的意義-實現多粒度鎖機制,例如InnoDB存儲引擎,它同時支持表級鎖和行級鎖。若沒有意向鎖,我們對錶加表級X鎖之前,確定表中是否有行級X鎖的方法只能是遍歷每一行的索引上是否有行級X鎖(需要檢查是否有行級X鎖的原因是避免死鎖,若表上有X鎖,行記錄的索引上也有X鎖,便相互鎖死)。當我們使用意向鎖,我們只需確定表上是否有IX鎖,便確定是否能對錶加X鎖。意向鎖是InnoDB自動加的,不需要用戶干預。

表級鎖兼容圖

是否兼容 X IX S IS
X
IX
S
IS

如果一個鎖與現有鎖兼容,則將其授予請求的事務,但如果與現有鎖衝突,則不授予該鎖。事務等待直到衝突的現有鎖被釋放。如果鎖定請求與現有鎖定發生衝突,並且由於可能導致死鎖而無法被授予,則會發生錯誤。

行級鎖

InnoDB默認使用的鎖就是行級鎖,但行級鎖是加在每一行數據的索引項上面,只有通過索引條件檢索數據才使用行級鎖,否則InnoDB將使用表鎖,當然這整個過程都是InnoDB全自動完成,無需用戶插手。行級鎖從功能性角度分爲排他鎖和共享鎖,特性如表級排他鎖和共享鎖一樣,這裏就不做過多闡述。

InnoDB中行級鎖從鎖的範圍來分有三種鎖(算法),InnoDB使用Next-Key Locks進行行記錄的操作,但當檢索條件爲唯一索引時(只查出一條行記錄),將Next-Key Locks降爲Record Locks,因爲Gap Locks在這種情況下會失效。

(1)Record Locks: 記錄鎖,記錄鎖是對記錄的索引項的鎖定,加鎖之後不能修改或者刪除該記錄。因爲InnoDB使用的是聚集索引,因此表中一定存在索引(即使用戶沒定義主鍵或者不存在非空唯一索引,InnoDB還是會生成一個隱藏的聚集索引)
(2)Gap Locks: 間隙鎖,間隙鎖是對索引記錄之間的間隙的鎖定,被間隙鎖鎖定的範圍內所有行記錄之間的間隙均被鎖定,用來防止事務在間隙鎖鎖定的範圍之內進行數據的插入。例如以下查詢語句就會在第一個值爲10的記錄之前以及最後一個值爲20的記錄之後加上間隙鎖,把c1值在10與20之間的所有行記錄都鎖住,然後無法插入c1值在10和20之間的記錄。注意共享和排他間隙鎖之間沒有區別,它們彼此不衝突,並且執行相同的功能。我們可以在同一間隙中加多個共享間隙鎖或者排他間隙鎖,因爲間隙鎖只要保證沒有事務在這個範圍內插入數據即可,因此間隙鎖可以解決幻增(幻讀產生原因包括幻增和幻減)問題。當查詢的條件是唯一索引且只需要檢索一行時,不會使用間隙鎖(這不包括搜索條件僅包含多列唯一索引的某些列的情況;在這種情況下確實發生了間隙鎖定)。關閉gap鎖的方法-將事務隔離級別設置爲RC 。

SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 

(3)Next-Key Locks: Next-Key Locks是Record Locks和Gap Locks的結合,是索引記錄上的記錄鎖定和索引記錄之前的間隙上的間隙鎖定的組合。Next-Key Locks可以解決幻讀問題。InnoDB使用Next-Key鎖和REPEATABLE-READ隔離級別,達到了可串行化的隔離級別效果。

死鎖

上文就提到使用行級鎖可能會導致死鎖,這是因爲事務在給多行數據上鎖時,不是一次性全部加鎖,而是一行行的加鎖,因此當事務對多個表的行記錄或者一個表的多行記錄進行操作時就可能會造成死鎖。如事務A需要給同一個表的S1行記錄和S2行記錄上排他鎖,事務B也需要給S1行記錄和S2行記錄上排他鎖。事務A先成功給S1加鎖,事務B成功給S2加鎖,然後都在阻塞等待對方釋放鎖,這時就產生了死鎖。或者是事務A先查詢S3行記錄,此時就上了S鎖,之後事務B來刪除S3行記錄,但因爲已經上了S鎖,因此事務B阻塞等待,但此時事務A又來刪除S3行記錄,由於B先在等待S鎖釋放,因此事務A等待B先獲得排他鎖,因此事務A與事務B相互阻塞,形成死鎖。但發生死鎖後,InnoDB一般都能自動檢測到,並使更小的事務回退並釋放鎖,另一個事務獲得鎖,繼續完成事務。但死鎖檢測會導致速度變慢,性能變差,因此對於一些可以避免的死鎖,我們是需要去解決的,而不是完全靠存儲引擎。

InnoDB解決死鎖的方法

(1)使用SHOW ENGINE INNODB STATUS命令來確定最新死鎖的原因,以幫助調整應用程序以避免死鎖。
(2)如果頻繁出現死鎖警告,一定不要大意多的調試信息。MySQL錯誤日誌中記錄了所有的關於死鎖的信息。完成調試後,請禁用此選項。
(3)寫事務語句時,保持事務小且持續時間短,減少死鎖發生概率。
(4)及時將未更改的數據進行提交,不要長時間保持交互式mysql會話打開卻沒提交事務。
(5)修改事務中的多個表或同一表中的不同行記錄時,每次都要以一致的順序執行這些操作。
(6)在進行檢索時選擇最優的索引。那查詢需要掃描較少的索引記錄,因此設置鎖也會少些,不容易產生死鎖。
(7)實在不行使用表級鎖,表級鎖不會產生死鎖。

樂觀鎖和悲觀鎖

樂觀鎖和悲觀鎖與其說是兩種鎖,不如說是兩種思想,樂觀鎖樂觀的認爲不會發生讀寫衝突,若發生衝突了我再來補救,因此樂觀鎖是先進行操作,在提交更新時,再進行檢測,若檢測發現產生了衝突,再將錯誤信息返回給用戶,讓用戶來決定是阻塞等待還是放棄更改;而悲觀鎖則悲觀的認爲不加約束一定會產生衝突,因此悲觀鎖在進行操作之前會先加鎖,再操作,因此導致了併發事務中其它等待鎖釋放的事務阻塞等待,一定程度影響了併發性能。

悲觀鎖

數據庫默認的鎖機制其實實現的就是悲觀鎖,排他鎖和共享鎖其實就是悲觀鎖的不同實現。悲觀鎖依賴於數據庫底層的鎖機制來保證數據的一致性和完整性。但悲觀鎖並不是萬能的,很多場景並不適用。悲觀鎖在進行操作之前會先加鎖,再操作,因此導致了併發事務中其它等待鎖釋放的事務阻塞等待,一定程度影響了併發性能,因此一些較大的事務,可以選擇使用樂觀鎖。

樂觀鎖

樂觀鎖是先操作再檢測,檢測出現問題再進行補救。需要用戶自行實現,一般有兩種實現方式,一是版本號,另外一種是時間戳
(1)版本號實現 :使用數據版本號機制實現,這是樂觀鎖最常用的一種實現方式,一般是通過爲數據庫表增加一個數字類型的 “version” 字段來實現。每次讀取數據時,將version字段的值一同讀出; 每次成功提交更新一次,對此version值加一。當我們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次讀取出來的version值進行比對,如果數據庫表當前版本號與第一次取出來的version值相等,則予以更新,並將version值加一,否則就更新失敗。
(2)時間戳實現: 原理與版本號實現相同,在數據庫表中增加一個時間戳類型的字段,每次讀取數據時,將該字段讀取出來,在更新提交的時候檢查當前數據庫中數據的時間戳和自己更新前讀取到的時間戳進行對比,如果一致則予以更新,並將現在的時間戳替換原時間戳,否則就更新失敗。

InnoDB鎖機制總結

(1) InnoDB支持行級鎖和表級鎖,默認使用行級鎖,但因爲行級鎖是建立在索引之上的,若是檢索條件沒有使用索引,則會使用表級鎖。
(2) 意向鎖的作用是爲了實現多粒度鎖機制,讓表級鎖和行級鎖共存。
(3)InnoDB的行級鎖使用算法是Next-Key Locks,在進行範圍查詢或者範圍更新時同時鎖住記錄和間隙,可以用來避免幻讀問題。
(4)InnoDB的隔離級別是RR(可重複讀),InnoDB通過MVCC和鎖機制來實現RR,MVCC的作用是避免了讀寫操作之間的事務阻塞,提高了併發性能,通過鎖解決了寫寫操作之間的衝突。只有使用普通的select纔會使用MVCC進行查詢,也就是快照讀。如果是查詢使用的是select … lock in share mode,select … for update或者使用的是insert,update,delete 語句,則還是會使用悲觀鎖進行當前讀,防止讀寫、寫寫衝突,缺點是會導致讀寫阻塞。

如果對InnoDB的MVCC實現原理和其工作機制以及快照讀、當前讀等知識不熟悉可以翻閱我的另外一篇博文-《InnoDB的MVCC實現原理(InnoDB如何實現MVCC以及MVCC的工作機制)》

(5)InnoDB是MVCC機制和鎖機制一起工作來進行併發控制的,使用普通的select進行查詢時,不會加S鎖,而是通過MVCC機制進行快照讀,避免了其他事務寫操作阻塞等待,不加S鎖也意味着當該行數據正在被寫時,進行讀操作的事務也無需等待可以進行快照讀。使用select … lock in share mode,select … for update進行查詢時就會用到鎖機制中的排他鎖或者共享鎖進行阻塞讀操作,或者是使用的insert,update,delete 等更新語句時,也會使用排他鎖將數據鎖住。要注意的是InnoDB使用的是Next-Key Locks算法,當它使用行排他鎖或者行共享鎖鎖住相應的行記錄時,不僅鎖記錄項,而且鎖住記錄項中間的間隙,防止幻讀。
(6)InnoDB使用Next-Key Locks和RR(可重複讀)隔離級別來達到了可串行化級別,防止了丟失更新、髒讀、不可重複讀、幻讀等問題。
(7)當然InnoDB的鎖機制除了上述這些鎖,還包括插入意圖鎖、自增鎖等,讀者感興趣的話可以自行翻閱官方文檔

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