【眼見為實】自己動手實踐理解數據庫REPEATABLE READ && Next-Key Lock


[REPEATABLE READ]

首先設置數據庫隔離級別為可重復讀(REPEATABLE READ):

set global transaction isolation level REPEATABLE READ ;
set session transaction isolation level REPEATABLE READ ; 

[REPEATABLE READ]能解決的問題之一

[REPEATABLE READ]隔離級別解決了不可重復讀的問題,一個事務中多次讀取不會出現不同的結果,保證了可重復讀。
還是上一篇中模擬不可重復讀的例子:
事務1

START TRANSACTION;
① SELECT sleep(5);
② UPDATE users SET state=1 WHERE id=1;
COMMIT;

事務2

START TRANSACTION;
① SELECT * FROM users WHERE id=1;
② SELECT sleep(10);
③ SELECT * FROM users WHERE id=1;
COMMIT;  

事務1先於事務2執行。
事務1的執行信息

[SQL 1]START TRANSACTION;
受影響的行: 0
時間: 0.000s

[SQL 2]
SELECT sleep(5);
受影響的行: 0
時間: 5.001s

[SQL 3]
UPDATE users SET state=1 WHERE id=1;
受影響的行: 1
時間: 0.000s

[SQL 4]
COMMIT;
受影響的行: 0
時間: 0.062s

事務2的執行信息

[SQL 1]
    SELECT * FROM users WHERE id=1;
受影響的行: 0
時間: 0.000s

[SQL 2]
    SELECT sleep(10);
受影響的行: 0
時間: 10.001s

[SQL 3]
    SELECT * FROM users WHERE id=1;
受影響的行: 0
時間: 0.001s

[SQL 4]
    COMMIT;
受影響的行: 0
時間: 0.001s

執行結果
markmark
結論
可重復讀[REPEATABLE READ]隔離級別解決了不可重復讀的問題。

分析
可重復讀[REPEATABLE READ]隔離級別能解決不可重復讀根本原因其實就是前文講過的read view的生成機制和[READ COMMITTED]不同。
[READ COMMITTED]:只要是當前語句執行前已經提交的數據都是可見的。
[REPEATABLE READ]:只要是當前事務執行前已經提交的數據都是可見的。
在[REPEATABLE READ]的隔離級別下,創建事務的時候,就生成了當前的global read view,一直維持到事務結束。這樣就能實現可重復讀。

在模擬不可重復讀的事務中,事務2創建時,會生成一份read view。事務1的事務id trx_id1=1,事務2的事務id trx_id2=2。假設事務2第一次讀取數據前的此行數據的事務trx_id=0。事務2中語句①執行前生成的read view為{1},trx_id_min=1,trx_id_max=1。因為trx_id(0)<trx_id_min(1),該行記錄的當前值可見,將該可見行的值state=0返回。因為在[REPEATABLE READ]隔離級別下,只有在事務創建時才會重新生成read view ,事務2第二次讀取數據之前事務1對數據進行了更新操作,此行數據的事務trx_id=1。trx_id_min(1)=trx_id(1)=trx_id_max(1),此時此行數據對事務2是不可見的,從該行記錄的DB_ROLL_PTR指針所指向的回滾段中取出最新的undo-log的版本號的數據,將該可見行的值state=0返回。所以事務2第二次讀取數據時的處理和第一次讀取時是一致的,讀取的state=0。數據是可重復讀的。

從事務1的執行信息中的[SQL 3]我們可以得知,[REPEATABLE READ]隔離級別讀操作也是不加鎖的。因為如果讀需要加S鎖的話,是在事務結束時釋放S鎖的。那么事務1[SQL 3]進行更新操作申請X鎖的時候便會等待事務2的S鎖釋放。現實並不是。

我們知道,MySql的InnoDB引擎是通過MVCC的方式在保證數據的安全性的同時,實現了讀的非阻塞。MVCC模式需要額外的存儲空間,需要做更多的行檢查工作;但是保證了讀操作不用加鎖,提升了性能,是一種典型的犧牲空間換取時間思想的實現。需要注意的是,MVCC只在[READ COMMITTED]和[REPEATABLE READ]兩個隔離級別下工作。其他兩個隔離級別都和MVCC不兼容,因為[READ UNCOMMITTED]總是讀取最新的數據行,而不是符合當前事務版本的數據行。而[SERIALIZABLE]則會對所有讀取的行都加鎖。

通過親自實踐模擬分析[READ COMMITTED]和[REPEATABLE READ]兩個隔離級別的工作機制,我們也能深刻的體會到各個數據庫引擎實現各種隔離級別的方式並不是和標准sql中的封鎖協議定義一一對應的。

[REPEATABLE READ]能解決的問題之二

幻讀其實是不可重復讀的一種特殊情況。不可重復讀是對數據的修改更新產生的;而幻讀是插入或刪除數據產生的。所謂的幻讀有2種情況,一個事物之前讀的時候,讀到一條記錄,再讀發現記錄沒有了,被其它事務刪了,另外一種是之前讀的時候記錄不存在,再讀發現又有這條記錄,其它事物插入了一條記錄。

事務1

START TRANSACTION;
SELECT * FROM users;
SELECT sleep(10);
SELECT * FROM users;
COMMIT;

事務2

START TRANSACTION;
SELECT sleep(5);
INSERT INTO users VALUES(2,'song',2);
COMMIT;

執行結果

1.預期結果

markmark

2.實際結果

markmark

事務1中並沒有讀取到事務2新插入的數據,並沒有發生幻讀現象。這有點出乎我的意料,難道Mysql[REPEATABLE READ]隔離級別能解決幻讀問題?按照封鎖協議定義,三級封鎖協議是解決不了幻讀的問題的。只有最強封鎖協議,讀和寫都對整個表加鎖,才能解決幻讀的問題。但是這樣做相當於所有的操作串行化,數據庫支持並發的能力會變得極差。所以Mysql的InnoDB引擎通過自己的方式在[REPEATABLE READ]隔離級別上解決了幻讀的問題,下面我們探究一下InnoDB引擎是如何解決幻讀問題的。

分析
InnoDB有三種行鎖的算法:
1.Record Lock:單個行記錄上的鎖。
2.Gap Lock:間隙鎖,鎖定一個范圍,但不包括記錄本身。GAP鎖的目的,是為了防止同一事務的兩次當前讀,出現幻讀的情況。
3.Next-Key Lock:1+2,鎖定一個范圍,並且鎖定記錄本身。主要目的是解決幻讀的問題。

在[REPEATABLE READ]級別下,如果查詢條件能使用上唯一索引,或者是一個唯一的查詢條件,那么僅加行鎖(通過唯一的查詢條件查詢唯一行,當然不會出現幻讀的現象);如果是一個范圍查詢,那么就會給這個范圍加上 Gap鎖或者 Next-Key鎖 (行鎖+Gap鎖)。理論上不會發生幻讀

驗證一下Gap Lock和Next-Key Lock的存在

我們可以通過自己操作來驗證一下Gap Lock和Next-Key Lock的存在。
首先我們需要給state字段加上索引。然后准備幾條數據,如下圖:
mark
事務1

START TRANSACTION;  
① SELECT * FROM users WHERE state=3 for UPDATE;

事務2

[SQL]INSERT INTO users VALUES(5,'song',1);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(6,'song',2);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(6,'song',3);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(6,'song',4);
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction

[SQL]INSERT INTO users VALUES(5,'song',0);
受影響的行: 1
時間: 0.120s

[SQL]INSERT INTO users VALUES(6,'song',5);
受影響的行: 1
時間: 0.195s

[SQL]INSERT INTO users VALUES(7,'song',7);
受影響的行: 1
時間: 0.041s

因為InnoDB對於行的查詢都是采用了Next-Key Lock的算法,鎖定的不是單個值,而是一個范圍(GAP)。上面索引值有1,3,5,8,其記錄的GAP的區間如下:
(-∞,1],(1,3],(3,5],(5,8],(8,+∞)。是一個左開右閉的空間。需要注意的是,InnoDB存儲引擎還會對輔助索引下一個鍵值加上Gap Lock。事務1語句①鎖定的范圍是(1,3],下個鍵值范圍是(3,5],所以插入1~4之間的值的時候都會被鎖定,要求等待,等待超過一定時間便會進行超時處理(Mysql默認的超時時間為50秒)。插入非這個范圍內的值都正常。

[REPEATABLE READ]讀到底加不加鎖?

當我理解了[REPEATABLE READ]隔離級別是如何解決幻讀問題時,隨即產生了另一個疑問。[READ COMMITED]和[REPEATABLE READ]通過MVCC的方式避免了讀操作加鎖的問題,但是[REPEATABLE READ]又為了解決幻讀的問題加Gap Lock或Next-Key Lock。那么問題來了,[REPEATABLE READ]讀到底加不加鎖?我對這個問題是百思不得其解,直到讀到了這篇文章才算理解了一些。

我們可以思考一下如果InnoDB對普通的查詢也加了鎖,那和序列化(SERIALIZABLE)的區別又在哪里呢?我的理解是InnoDB提供了Next-Key Lock,但需要應用自己去加鎖。這里又涉及到一致性讀(快照讀)和當前讀。如果我們選擇一致性讀,也就是MVCC的模式,讀就不需要加鎖,讀到的數據是通過Read View控制的。如果我們選擇當前讀,讀是需要加鎖的,也就是Next-Key Lock,其他的寫操作需要等待Next-Key Lock釋放才可寫入,這種方式讀取的數據是實時的。

一致性讀很好理解,讀不加鎖,不堵塞讀。當前讀對讀加鎖可能比較難理解,我們可以通過一個例子來理解一下:

事務1									   事務2
START TRANSACTION; 						  START TRANSACTION; 
SELECT * FROM users;
										INSERT INTO users VALUES (2, 'swj',2);
                                		   COMMIT;
SELECT * FROM users;
SELECT * FROM users LOCK IN SHARE MODE;
SELECT * FROM users FOR UPDATE;

執行結果

mysql> SELECT * FROM users;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
+----+------+-------+
1 row in set (0.04 sec)

mysql> SELECT * FROM users;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
+----+------+-------+
1 row in set (0.08 sec)

mysql> SELECT * FROM users LOCK IN SHARE MODE;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
|  2 | swj  |     2 |
+----+------+-------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM users FOR UPDATE;
+----+------+-------+
| id | name | state |
+----+------+-------+
|  1 | swj  |     1 |
|  2 | swj  |     2 |
+----+------+-------+
2 rows in set (0.00 sec)

結論MVCC是實現的是快照讀,Next-Key Lock是對當前讀。MySQL InnoDB的可重復讀並不保證避免幻讀,需要應用使用加鎖讀來保證,而這個加鎖讀使用到的機制就是Next-Key Lock


本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。

如果您認為還不錯,不妨點擊一下下方的[【推薦】](javascript:void(0)😉按鈕,謝謝支持。

轉載與引用請注明出處。


免責聲明!

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



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