[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
複製程式碼
執行結果:
結論: 可重複讀[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.預期結果
2.實際結果
事務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欄位加上索引。然後準備幾條資料,如下圖:
事務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。