這裏給出 mysql 幻讀的比較形象的場景:mysql
users: id 主鍵sql
1、T1:select * from users where id = 1; 2、T2:insert into `users`(`id`, `name`) values (1, 'big cat'); 3、T1:insert into `users`(`id`, `name`) values (1, 'big cat');
T1 :主事務,檢測表中是否有 id 爲 1 的記錄,沒有則插入,這是咱們指望的正常業務邏輯。spa
T2 :干擾事務,目的在於擾亂 T1 的正常的事務執行。3d
在 RR 隔離級別下,一、2 是會正常執行的,3 則會報錯主鍵衝突,對於 T1 的業務來講是執行失敗的,這裏 T1 就是發生了幻讀,由於T1讀取的數據狀態並不能支持他的下一步的業務,見鬼了同樣。code
在 Serializable 隔離級別下,1 執行時是會隱式的添加 gap 共享鎖的,從而 2 會被阻塞,3 會正常執行,對於 T1 來講業務是正確的,成功的扼殺了擾亂業務的T2,對於T1來講他讀取的狀態是能夠拿來支持業務的。blog
因此 mysql 的幻讀並不是什麼讀取兩次返回結果集不一樣,而是事務在插入事先檢測不存在的記錄時,驚奇的發現這些數據已經存在了,以前的檢測讀獲取到的數據如同鬼影通常。事務
這裏要靈活的理解讀取的意思,第一次select是讀取,第二次的 insert 其實也屬於隱式的讀取,只不過是在 mysql 的機制中讀取的,插入數據也是要先讀取一下有沒有主鍵衝突才能決定是否執行插入。it
不可重複讀側重表達 讀-讀,幻讀則是說 讀-寫,用寫來證明讀的是鬼影。io
下面給出幻讀的例子:table
設置隔離級別爲 Read Repeatable,開啓兩個事務 t1和t2
原始表以下:
在t1中,首先查看id=C的數據,爲空:
mysql> select * from amount where id = 'C'; Empty set (0.00 sec)
而後在t2中插入id='C'的數據,此時咱們發現插入成功了
mysql> insert into amount values('C',1000); Query OK, 1 row affected (0.01 sec)
而後,咱們在t1中插入id='C'的數據,能夠看到一直處於等待狀態,直到t2的事務被提交或者超時。
當t2的事務被提交後,t1中會報主鍵重複的錯誤:
mysql> insert into amount values('C',1000); ERROR 1062 (23000): Duplicate entry 'C' for key 'PRIMARY'
這就是幻讀現象,咱們在t1中查詢id='C'的數據時顯示數據不存在,可是因爲t2中插入了id=C的數據,致使了t1再想插入時出現了主鍵重複的錯誤,t2成功擾亂了t1的事務。
咱們看看再Serializable的級別下是如何的:
在t1中,首先查看id=C的數據,爲空:
mysql> select * from amount where id = 'C'; Empty set (0.00 sec)
t2插入數據,發現跟上面不一樣的是,t2阻塞了。
這時在t1中插入數據,成功插入:
mysql> insert into amount values('C',1000); Query OK, 1 row affected (0.02 sec)
當t1提交之後,此次輪到t2出現了主鍵重複的錯誤。
從結果能夠知道,t1的事務並無受到t2事務的擾亂,即在Serializable的隔離級別下沒有出現幻讀。
在上面兩個實驗中咱們發現,repeatable read是沒法避免幻讀的,可是,在某種狀況下,它卻能解決幻讀問題。
下面看例子1,查詢的記錄不存在的狀況
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount where id = 'E' for update; Empty set (0.00 sec) |
|
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ 4 rows in set (0.00 sec) |
|
![]() 鎖住了。 |
|
mysql> insert into amount values('E',1000); Query OK, 1 row affected (0.01 sec) |
|
commit | |
mysql> insert into amount values('E',1000); ERROR 1062 (23000): Duplicate entry 'E' for key 'PRIMARY' |
|
commit; |
使用select .. for update鎖住,而後再insert,能夠避免幻讀。
其實,即便在t2中,插入id不爲"E"的記錄,也是會阻塞的(鎖住),依然要等待t1提交後才能輪到t2工做。
例2 ,當查詢的結果已經存在
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | +----+-------+ |
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | +----+-------+ |
mysql> select * from amount where id = 'A' for update; +----+-------+ | id | money | +----+-------+ | A | 100 | +----+-------+ |
|
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | +----+-------+ |
|
mysql> insert into amount values('C',1000); Query OK, 1 row affected (0.01 sec) |
|
commit; | commit; |
以上例子說明,for update時候,id爲主鍵,RR策略時候,鎖住了的條件符合的行,可是若是條件找不到任何列,鎖住的是整個表,所以當t1查詢到的記錄爲空時,在t2想插入該主鍵記錄時是阻塞的;當t1查詢到的記錄非空時,除了該主鍵記錄以外,能夠在其餘事務插入任何不存在的主鍵記錄而不阻塞。
例3,範圍查詢的狀況
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount where id < 'M' for update; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ |
|
mysql> insert into amount values('X',1000); Query OK, 1 row affected (0.01 sec) |
|
mysql> select * from amount ; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | | M | 1000 | | N | 1000 | +----+-------+
|
|
![]() 鎖住了 |
|
mysql> select * from amount ; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | | M | 1000 | | N | 1000 | +----+-------+
|
|
commit; | |
mysql> select * from amount ; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | | M | 1000 | | N | 1000 | +----+-------+ 可重複讀 |
能夠看到,用 id<'M' 加的鎖,只鎖住了 id< 'M' 的範圍,能夠成功添加id爲X的記錄,添加id爲'G'的記錄時就會等待鎖的釋放。
例4
t1 | t2 |
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ; | |
start transaction; | start transaction; |
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | +----+-------+
|
|
mysql> insert into amount values('D',1000); Query OK, 1 row affected (0.01 sec)
|
|
mysql> select * from amount; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | +----+-------+ 可重複讀 |
|
mysql> select * from amount lock in share mode; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ 加鎖,讀取的是最新值,當前讀 |
|
mysql> select * from amount for update; +----+-------+ | id | money | +----+-------+ | A | 100 | | B | 600 | | C | 1000 | | D | 1000 | +----+-------+ 加鎖,讀取的是最新值,當前讀 |
若是使用普通的讀,會獲得一致性的結果,若是使用了加鎖的讀,就會讀到「最新的」「提交」讀的結果。
自己,可重複讀和提交讀是矛盾的。在同一個事務裏,若是保證了可重複讀,就會看不到其餘事務的提交,違背了提交讀;若是保證了提交讀,就會致使先後兩次讀到的結果不一致,違背了可重複讀。
能夠這麼講,InnoDB提供了這樣的機制,在默認的可重複讀的隔離級別裏,能夠使用加鎖讀去查詢最新的數據。
結論:
MySQL InnoDB的可重複讀並不保證避免幻讀,須要應用使用加鎖讀來保證。而這個加鎖度使用到的機制就是next-key locks。
mysql 的重複讀解決了幻讀的現象,可是須要 加上