深入理解數據庫行鎖與表鎖

深入理解數據庫行鎖與表鎖

在上一章節中我們學習了數據庫的事務及其事務的隔離級別,但是數據庫是怎樣隔離事務的呢?這時候就牽連到了數據庫鎖。當插入數據時,就鎖定表,這叫做”鎖表”;當更新數據時,就鎖定行,這叫做”鎖行”。

鎖在數據網絡傳輸中是一個非常重要的概念,當多個用戶對數據庫進行操作時,會帶來數據不一致的情況,所以,鎖主要是在多用戶情況下保證數據庫數據完整性和一致性。

當然,數據庫中的鎖遠不止於上面提到的兩種。通常提及數據庫鎖,想必大家優先想到的,必然是樂觀鎖,數據庫樂觀鎖可以幫助我們解決很多問題,但數據庫中還有很多其它的鎖,總結一下大概有如下:悲觀鎖、樂觀鎖、表鎖、行鎖、臨間鎖、間隙鎖、記錄鎖、共享鎖、排他鎖、意向共享鎖、意向排他鎖。

上面一共提到了11種鎖,如果給它們進行分類,大抵可以按如下劃分:

img

img

樂觀鎖和悲觀鎖這個不用再多說了,相信大家也都是知道的。Mysql中的鎖機制基本上都是採用的悲觀鎖來實現的。我們先來看一下”行鎖”。

行鎖

顧名思義,行鎖就是一鎖鎖一行或者多行記錄,mysql的行鎖是基於索引加載的,所以行鎖是要加在索引響應的行上,即命中索引,如下圖所示:

img

如上圖所示,數據庫表中有一個主鍵索引和一個普通索引,Sql語句基於索引查詢,命中兩條記錄。此時行鎖一鎖就鎖定兩條記錄,當其他事務訪問數據庫同一張表時,被鎖定的記錄不能被訪問,其他的記錄都可以訪問到。

行鎖的特徵:鎖衝突概率低,併發性高,但是會有死鎖的情況出現。

我們使用代碼演示一下,看看行鎖的表現:我們還是使用上一篇文章中使用的數據庫,打開兩個窗口,我們在窗口A中根據id更新一條記錄,然後在窗口B中也執行相同的SQL語句看看

img

可以看到,窗口A先修改了id爲3的用戶信息後,還沒有提交事務,此時窗口B再更新同一條記錄,然後就提示Lock wait timeout exceeded; try restarting transaction ,由於窗口A遲遲沒有提交事務,導致鎖一直沒有釋放,就出現了鎖衝突,而窗口B一直在等待鎖,所以出現了超過鎖定超時的警告了。

但是,此時我們如果去更新id爲3它旁邊的記錄看看會出現怎樣的情況,我們新打開一個窗口更新id爲2的記錄看看。

img

可以看到,在窗口B中更新id爲3的記錄報錯,但是在窗口C中我們可以更新id爲2的記錄,這說明此時鎖定了id爲3的記錄但是並沒有鎖定它旁邊的記錄。

表鎖

顧名思義,表鎖就是一鎖鎖一整張表,在表被鎖定期間,其他事務不能對該表進行操作,必須等當前表的鎖被釋放後才能進行操作。表鎖響應的是非索引字段,即全表掃描,全表掃描時鎖定整張表,sql語句可以通過執行計劃看出掃描了多少條記錄。

img

由於表鎖每次都是鎖一整張表,所以表鎖的鎖衝突機率特別高,表鎖不會出現死鎖的情況。

和上面一樣,我們通過代碼演示一下,看看錶鎖的表現,我們打開兩個窗口,在窗口A中更新一條記錄,條件爲非索引字段,不提交事務,然後在窗口B中任意再更新一條記錄,我們看看會出現怎樣的現象:

img

上面,我們分別驗證了一下mysq的行鎖和表鎖,我們可以看到,當更新數據庫數據時,如果沒有觸發索引,則會鎖表,鎖表後再對錶做任何變更操作都會導致鎖衝突,所以表鎖的鎖衝突概率較高。

在mysql中,行鎖又衍生了其他幾種算法鎖,分別是 記錄鎖、間隙鎖、臨鍵鎖;我們依次來看看這三種鎖,什麼是記錄鎖呢?

記錄鎖

上面我們找到行鎖是命中索引,一鎖鎖的是一張表的一條記錄或者是多條記錄,記錄鎖是在行鎖上衍生的鎖,我們來看看你記錄鎖的特徵:

記錄鎖:記錄鎖鎖的是表中的某一條記錄,記錄鎖的出現條件必須是精準命中索引並且索引是唯一索引,如主鍵id,就像我們上面描述行鎖時使用的sql語句圖,在這裏就挺適用的。

img

圖中id是唯一索引,此時鎖的就是一條記錄,命中索引爲唯一索引,此時使用的鎖就是記錄鎖了。相信學習完行鎖後,再學習記錄鎖就簡單很多了吧。

間隙鎖

間隙鎖又稱之爲區間鎖,每次鎖定都是鎖定一個區間,隸屬行鎖。既然間隙鎖隸屬行鎖,那麼,間隙鎖的觸發條件必然是命中索引的,當我們查詢數據用範圍查詢而不是相等條件查詢時,查詢條件命中索引,並且沒有查詢到符合條件的記錄,此時就會將查詢條件中的範圍數據進行鎖定(即使是範圍庫中不存在的數據也會被鎖定),我們通過代碼演示一下:

首先,我們打開兩個窗口,在窗口A中我們根據id做一個範圍更改操作,不提交事務,然後在範圍B中插入一條記錄,該記錄的id值位於窗口A中的條件範圍內,我們看看運行效果:

img

如上所示,程序報錯:Lock wait timeout exceeded; try restarting transaction 。這就是間隙鎖的作用。間隙鎖只會出現在可重複讀的事務隔離級別中,mysql5.7默認就是可重複讀。間隙鎖鎖的是一個區間範圍,查詢命中索引但是沒有匹配到相關記錄時,鎖定的是查詢的這個區間範圍,上述代碼中,所鎖定的區間就是 (1,3]這個區間,不包含1,但是包含3,並且不包含4,也就是說這裏是一個左開右閉的區間。

如果我們將mysql數據庫隔離級別修改爲不可重複讀,然後再運行一下上面代碼,看看會是怎樣的呢,我們來驗證一下間隙鎖只會出現在可重複讀的事務隔離級別中:

設置事務隔離級別爲不可重複讀
set session transaction isolation level read committed;
查看當前事務級別
SELECT @@tx_isolation

我們修改數據庫隔離級別後,然後將上面的代碼流程再走一遍看看:

img

可以看到,修改了數據庫隔離級別後,再次測試間隙鎖,發現間隙鎖沒有生效。我們可以通過rollback回滾事務。

臨鍵鎖

學習完間隙鎖後我們再來看看什麼是臨鍵鎖,mysql的行鎖默認就是使用的臨鍵鎖,臨鍵鎖是由記錄鎖和間隙鎖共同實現的,上面我們學習間隙鎖時,間隙鎖的觸發條件是命中索引,範圍查詢沒有匹配到相關記錄。而臨鍵鎖恰好相反,臨鍵鎖的觸發條件也是查詢條件命中索引,不過,臨鍵鎖有匹配到數據庫記錄

上面我們知道,間隙鎖所鎖定的區間是一個左開右閉的集合,而臨鍵鎖鎖定是當前記錄的區間和下一個記錄的區間,我們一起來看看:

img

img

從上圖我們可以看到,數據庫中只有三條數據1、5、7,當修改範圍爲1~8時,則鎖定的區間爲(1,+∞),鎖定額不單是查詢範圍,並且還鎖定了當前範圍的下一個範圍區間,此時,查詢的區間8,在數據庫中是一個不存在的記錄值,並且,如果此時的查詢條件是小於或等於8,也是一樣的鎖定8到後面的區間。

如果查詢的結尾是一個存在的值,此時又會怎樣呢?現在數據庫有三條數據id分別是1、5、7,我們查詢條件改爲大於1小於7再看看。

img

此時,我們可以看到,由於7在數據庫中是已知的記錄,所以此時的鎖定後,只鎖定了(1,7],7之後的數據都沒有被鎖定。我們還是可以正常插入id爲8的數據及其後面的數據。

所以,臨鍵鎖鎖定區間和查詢範圍後匹配值很重要,如果後匹配值存在,則只鎖定查詢區間,否則鎖定查詢區間和後匹配值與它的下一個值的區間。

但是,爲什麼會出現這種情況呢?爲什麼臨鍵鎖後匹配會這樣呢?在這裏,我們不妨看看mysql的索引是怎麼實現的,前面文章中有提到樹結構,mysql的索引是基於B+樹實現的,每個樹節點上都有多個元素,即關鍵字數,當我們的索引樹上只有1、5、7時,我們查詢1~8,這個時候由於樹節點關鍵字中並沒有8,所以就把8到正無窮的區間範圍都給鎖定了。

但是,爲什麼會出現這種情況呢?爲什麼臨鍵鎖後匹配會這樣呢?在這裏,我們不妨看看mysql的索引是怎麼實現的,前面文章中有提到樹結構,mysql的索引是基於B+樹實現的,每個樹節點上都有多個元素,即關鍵字數,當我們的索引樹上只有1、5、7時,我們查詢1~8,這個時候由於樹節點關鍵字中並沒有8,所以就把8到正無窮的區間範圍都給鎖定了。

那麼,如果我們數據庫中id有1、5、7、10,此時我們再模糊匹配id爲1~8的時候,由於關鍵字中並沒有8,所以找比8大的,也就找到了10,根據左開右閉原則,此時10也是被鎖定的,但是id爲11的記錄還是可以正常進行插入的。這裏我沒有測試,感興趣的朋友可以下去自己嘗試一下。我們的鎖都是基於索引的,而mysql中索引的底層是使用的B+樹,我們瞭解了B+樹的特性後,就更容易理解很多遇到鎖的問題了。

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