隔離級別、幻讀、Gap Lock、Next-Key Lock

前面我寫了很多Mysql相關的知識點,到這一篇稍微可以串一下了,從SQL執行流程、MVCC到鎖,很多時候可能覺得對於間隙鎖和Next-Key Lock好像已經理解了,但是好像又覺得理解差那麼一點意思,這篇文章從頭來梳理一下概念,明確一下這些知識。

首先,對於Mysql來說實現了兩種行級鎖:

共享鎖:允許事務讀一行數據,一般記爲S,也稱爲讀鎖

排他鎖:允許事務刪除或者更新一行數據,一般記爲X,也稱爲寫鎖

關於讀寫鎖的互斥性,應該都很清楚,讀鎖只能和讀鎖兼容,其他場景都無法兼容,這裏不再贅述吧。

隔離級別

繼續回顧下關於Mysql的4個隔離級別:

讀未提交Read Uncommitted:能讀到其他事務還沒有提交的數據,這種現象叫做髒讀。

讀已提交Read Committed:只會讀取其他事務已經提交的數據,所以不會產生RC的髒讀問題。所以又帶來一個問題叫做不可重複讀,一個事務中兩次一樣的SQL查詢可能查到的結果不一樣。

可重複讀Repeatable Read:RR是Mysql的默認隔離級別,一個事務中兩次SQL查詢總是會查到一樣的結果,不存在不可重複讀的問題,但是還是會有幻讀的問題。

串行Serializable:串行場景沒有任何問題,完全串行化的操作,讀加讀鎖,寫加寫鎖。

幻讀、Next-Key Lock、MVCC

簡單的回顧完了基礎,那麼我們看看RR級別下還會存在的幻讀到底是什麼問題,Mysql官方文檔這樣描述的:

The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.

翻譯過來就是,幻讀指的是同一事務下,不同的時間點,同樣的查詢,得到不同的行記錄的集合。

如果說一個select執行了兩次,但是第二次比第一次多出來行記錄,這就是幻讀。

所以,對於幻讀來說那一定是新增插入的數據!

比如說在一個事務內,先查詢select * from user where age=10 for update,得到的結果是id爲[1,2,3]的記錄,再次執行查詢,得到了結果爲[1,2,3,4]的記錄,這是幻讀。

那怎麼解決幻讀的問題?以前我在文章裏說解決幻讀的原理是MVCC(MVCC原理看這裏)很多網上的文章也有這麼寫的,其實不能說錯,但是肯定也是不太對的,準確地來說應該是通過MVCC+Next-Key Lock的方式才解決了幻讀的問題。

對於MVCC中的讀可以分爲兩種,分別叫做快照讀當前讀(這個當前讀的說法我在書裏翻了半天也沒有找到,但是看網上一堆資料和大佬都叫當前讀,那麼我們就叫當前讀吧,你知道的話可以告訴我哪本書有這個稱呼,Mysql我只看見Lock reading或者鎖定讀的叫法,有的也說鎖定讀就是當前讀,但是並沒有找到當前讀這種稱呼的出處在哪兒)。

快照讀就是簡單的select查詢,查詢的都是快照版本,這個場景下因爲都是基於MVCC來查詢快照的某個版本,所以不會存在幻讀的問題,也可以認爲是解決了幻讀的方案之一,對於RC級別來說,因爲每次查詢都重新生成一個read view,也就是查詢的都是最新的快照數據,所以會可能每次查詢到不一樣的數據,造成不可重複讀,而對於RR級別來說只有第一次的時候生成read view,查詢的是事務開始的時候的快照數據,所以就不存在不可重複讀的問題,當然就更不可能有幻讀的問題了。

所以,現在我們說幻讀,其實不是指快照讀的場景,而是指的是當前讀的場景。

當前讀指的是lock in share modefor update 、insertupdatedelete這些需要加鎖的操作。對於MVCC來說就是解決的快照讀的場景,而對於當前讀那麼就是Next-Key Lock要解決的事情。

那麼Next-Key Lock是什麼?怎麼解決的幻讀?

行鎖有寫鎖X和讀鎖S兩種,實際上行鎖有3種實現算法,Next-Key Lock是其中之一。

第一種叫做Record Lock,字面意思,行記錄的鎖,實際上指的是對索引記錄的鎖定。

比如執行語句select * from user where age=10 for update,將會鎖住user表所有age=10的行記錄,所有對age=10的記錄的操作都會被阻塞。

第二種都比較熟悉,叫做Gap Lock,也就是間隙鎖,它用於鎖定的索引之間的間隙,但是不會包含記錄本身。

比如語句select * from user where age>1 and age<10 for update,將會鎖住age在(1,10)的範圍區間,此時其他事務對該區間的操作都會被阻塞。

間隙鎖是可重複讀RR隔離級別下特有的,另外還有幾種場景也會不使用間隙鎖。

  1. 事務隔離級別設置爲不可重複讀RC ,這樣肯定沒有間隙鎖了。
  2. Innodb_locks_unsafe_for_binlog設置爲1
  3. 另外一種情況適用於主鍵索引或者唯一索引的等值查詢條件,比如select * from user where id=1id是主鍵索引,這樣只使用Record Lock就可以了,因爲能唯一鎖定一條記錄,所以沒有必要再加間隙鎖了,這是鎖降級的過程。

而第三種Next-Key Lock實際上就是相當於Record Lock+Gap Lock的組合。比如索引有10,20,30幾個值,那麼被鎖住的區間可能會是(-∞,10],(10,20],(20,30],(30,+∞)。

解決幻讀

上一篇關於更新SQL執行過程我們已經對這個基礎有了一定的瞭解,在這裏我們去掉和這裏內容無關的一些日誌的細節,把給數據加鎖的流程加入進去,這樣通過SQL執行可以更好地理解Next-Key Lock到底是如何解決幻讀的,執行過程如下:

  1. 首先第一步Server層會來查詢數據
  2. 存儲引擎根據查詢條件查到數據之後對數據進行加鎖,Record Lock或者間隙鎖,然後返回數據
  3. Server層拿到數據之後調用API去存儲引擎更新數據
  4. 最後存儲引擎返回結果,流程結束

搞一張表說明一下,user表有4個字段,id是主鍵索引,name是唯一索引,age是普通索引,city沒有索引,然後插入一些測試數據,下面區分一下幾種情況來說明是怎麼加Next-Key Lock的,然後就知道爲啥會沒有幻讀的問題了。

沒有索引

更新語句update user set city='nanjing' where city='wuhan'會發生什麼?

因爲city是沒有索引的,所以存儲引擎只能給所有的記錄都加上鎖,然後把數據都返回給Server層,然後Server層把city改成nanjing,再更新數據。

因此,首先Record Lock會鎖住現有的7條記錄,間隙鎖則會對主鍵索引的間隙全部加上間隙鎖。

所以,更新的時候沒有索引是非常可怕的一件事情,相當於把整個表都給鎖了,那表都給鎖了當然不存在幻讀了。

普通索引

我們再假設一個語句select * from user where age=20 for update

因爲age是一個普通索引,存儲引擎根據條件過濾查到所有匹配age=20的記錄,給他們加上寫鎖,間隙鎖會加在(10,20),(20,30)的區間上,因此現在無論怎樣都無法插入age=20的記錄了

爲什麼要鎖定這兩個區間?如果不鎖定這兩個區間的話,那麼還能插入比如id=11,age=20或者id=21,age=20的記錄,這樣就存在幻讀了。

(那實際上寫鎖不光是在會加在age普通索引上,還會加在主鍵索引上,因爲數據都是在主鍵索引下對吧,這個肯定也要加鎖的,爲了看起來簡單點,就不畫出來了)

唯一&主鍵索引

如果查詢的是唯一索引又會發生什麼呢?比如有查詢語句select * from user where name='b' for update

上面我們提到過,如果是唯一索引或者主鍵索引的話,並且是等值查詢,實際上會發生鎖降級,降級爲Record Lock,就不會有間隙鎖了。

因爲主鍵或者唯一索引能保證值是唯一的,所以也就不需要再增加間隙鎖了。

很顯然,是無法插入name=b的的記錄的,也不存在幻讀問題。

如果是範圍查詢比如id>1 and id<11呢,實際上也是一樣的鎖定方式,不再贅述。

相比稍微有點不同的是上面也說過,唯一索引不光鎖定唯一索引,還會鎖定主鍵索引,主鍵索引的話只要索引主鍵索引就行了。

總結

那最後說了這麼多,RR級別下不是都已經解決了幻讀的問題嗎,怎麼還說有幻讀的問題呢?

關於這個問題,可以看看這個報出的BUGhttps://bugs.mysql.com/bug.php?id=63870,回覆說了這不是BUG,這是符合隔離規範的設計,有興趣的自己看看吧。

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