首先設置數據庫隔離級別爲可重複讀(REPEATABLE READ):html
set global transaction isolation level REPEATABLE READ ;
set session transaction isolation level REPEATABLE READ ;
複製代碼
[REPEATABLE READ]隔離級別解決了不可重複讀的問題,一個事務中屢次讀取不會出現不一樣的結果,保證了可重複讀。 仍是上一篇中模擬不可重複讀的例子: 事務1:mysql
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
[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。數據是可重複讀的。session
從事務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中的封鎖協議定義一一對應的。
幻讀實際上是不可重複讀的一種特殊狀況。不可重複讀是對數據的修改更新產生的;而幻讀是插入或刪除數據產生的。所謂的幻讀有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的存在。 首先咱們須要給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]隔離級別是如何解決幻讀問題時,隨即產生了另外一個疑問。[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。