mysql中的幻讀與間隙鎖


一、數據庫隔離級別

一般來講,數據庫的隔離級別分為讀未提交、讀已提交(read commit,rc)、可重復讀(read reapeat,rr)、串行化四個級別。在mysql中默認隔離級別是rr。讀未提交存在臟讀問題(A事務讀到B事務未提交的數據),讀已提交存在重復讀問題(A事務讀取兩次數據a,期間a被B事務修改后提交,兩次數據不一致),可重復讀存在幻讀問題(A事務讀取兩次a=1的數據,期間B事務插入了一天a=1的數據,導致兩次讀取結果不一致)。

mysql中通過mvcc解決了臟讀和重復讀的問題,其中rr是在事務開啟時,創建read view。rc在每次查詢時創建read view。實際上rc隔離級別上不存在幻讀問題,所以可以使用rc+row格式的binlog組合避免幻讀。

rr隔離離別下,正常的查詢語句其實也不存在幻讀問題。但是一些update/delete語句采用的是當前讀,這會導致只有行鎖的情況下,產生幻讀,假設沒有間隙鎖,當前讀中也會出現重復讀的問題。mysql在rr隔離級別下解決幻讀問題,采用的是行鎖+間隙鎖,兩者合稱next-key lock。

我們知道mysql的行鎖實際上是加在索引上的,所以進入正題前,我還多聊幾句索引與行鎖。

二、行鎖

在innodb引擎下才有行鎖,

行鎖是兩階段鎖,在事務結束后才會釋放。行鎖有分為讀鎖和寫鎖,兩者關系如下圖:

                            image.png

跟行鎖沖突的是另一個行鎖。

三、索引

索引可以根據存儲的數據,分為主鍵索引與非主鍵索引。其中主鍵索引又稱聚簇索引,它的葉子節點存整行的值,實際數據就存儲在這個索引上,即表和索引存在一起。非主鍵索引又稱非聚簇索引(二級索引),它的葉子節點存的是主鍵的數據。所以使用非覆蓋的非聚簇索引會引起回表操作。

之前說過行鎖實際加載索引上,所以如果一個update語句的where條件中,如果沒有明確的索引時,會導致所有行均被加上行鎖(所有間隙也會加上間隙鎖)

四、幻讀是如何產生的

接下來我們先來聊一下幻讀是怎么產生的。我們可以先建表如下:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `c` (`c`)
) ENGINE=InnoDB;

insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

 

 

對於t表如果如下操作:

 

session A

session B

session C

t1

begin

update t set d=100 where c=5

 

 

t2

 

update t set c=5 where id=0

 

t3

 

 

insert into t values(50,5,50)

t4

commit

 

 

在只有行鎖的情況下。如果在t1,t2與t3之間,t4時刻分別用select * from t where c=5 for update進行當前讀,會發現當前讀中出現重復讀與幻讀的問題。但這並不是最嚴重的的問題,同樣的只考慮行鎖的情況下,事務提交順序是b->c->a。最后的實際數據是(5,5,100),(0,5,0),(100,5,100)

但是binlog中記錄的sql如下:

update t set c=5 where id=0  insert into t values(50,5,50)  update t set d=100 where c=5

這樣binlog執行后產生的數據是(5,5,100),(0,5,100),(50,5,100)。此處id為0和id為5的數據均和預期不一致。

為了解決上面的問題,innodb引入了間隙鎖(gap lock)

五、間隙鎖

索引上的記錄之間,是有間隙的,上文中的表插入數據后,其間隙如下:

        image.png

我們可以對上圖中的間隙加鎖,也就是間隙鎖,但是與行鎖不同,間隙鎖質檢不互斥,與間隙鎖沖突的是“往這個間隙中插入一個記錄”這個操作。

根據B+樹索引的搜索規則,非唯一索引滿足條件的數據與后一條數據,一定會被搜索到,唯一索引只會搜索到滿足條件的數據,而所有在索引上查找到的數據,都會加鎖。加鎖時,數據所在行加行鎖,所在行前一個間隙加間隙鎖,兩者組合成為next key lock。這里需要注意,比如上圖中(0,5]next-key lock中,也會阻止其他事物插入值為5的數據。

對於唯一索引的等值查詢,因為不可能插入其他等值的數據,所以next-key會退化為行鎖。

非唯一索引等值查詢,向兩側遍歷到最新值后,由於最新值一定不等於當前值,所以next-key會退化為間隙鎖。

下面我們一起看一下,innodb是如何用next-key lock解決幻讀的。

六、間隙鎖的運轉

6.1間隙鎖的基本使用

先回到我們之前看的問題,可以知道t1時刻會在索引c上有(0,5]和(5,10)的next-key鎖。所以t2時刻的事務實際無法提交,需要等待t4時刻session A提交后釋放next-key鎖,才能進行事務。

但實際上由於session B和session C雖然沒獲取到行鎖上的寫鎖,單兩者都獲取了(0,5]和(5,10)的間隙鎖,所以t4時刻后,兩者事務提交都會被阻塞,需要死鎖檢測釋放其中一個持有的間隙鎖后,才能依次執行。

之前索引中我們提到過非聚簇非覆蓋索引會引起回表,實際這個回表操作會額外對主鍵索引(聚簇索引)加鎖。如果通過lock in share mode先使用覆蓋索引,再使用主鍵索引時,主鍵索引上會會不產生鎖,所以可以修改數據,示例如下:

 

session A

session B

session C

t1

begin

select id from t where c=5 lock in share mode

 

 

t2

 

update t set d=100 where id=5

 

t3

 

 

insert into t values(7,7,7)

上面的例子中t2時刻session B的update語句是可以執行的,但是由於之前說過對普通鎖因c,t1時刻會產生(0,5]和(5,10)的next-key鎖,所以t3時刻的session C的insert語句是無法執行的。

如果t1時刻使用for update進行加鎖,系統會認為你接下來要更新數據,因此會順便給主鍵索引上滿足條件的行加上行鎖。這會導致t2時刻session B的update語句是無法執行。

6.2間隙鎖與二級索引

二級索引非唯一是,會按照主鍵進行排序,這會導致下面案例中的問題:

 

session A

session B

session C

t1

begin

update t set d=100 where c=10

 

 

t2

 

insert into t values(2,10,50)

 

t3

 

 

insert into t values(7,10,50)

t4

commit

 

 

上面案例中sessionb可以執行,但是session c不能執行。原因如下圖:

        image.png

6.3動態的間隙

 

session A

session B

session C

t1

begin

select * from t where c=11 for update

 

 

t2

 

delete from t where c=10

 

t3

 

 

insert into t values(10,10,10)

t4

commit

 

 

上面流程中session b可以執行,但是session c執行會失敗。原因是session a中是對(10,15)這個間隙加鎖,session b中刪除了c=10的數據后,會導致(5,10)和(10,15)這兩個間隙合並成了一個新的間隙(5,15),同時它繼承了之前的加鎖狀態,所以ssssion c中的插入操作會失敗


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM