糾錯
我猜,你們在各種博文中看到對于幻讀的解釋是這樣的:
一個事務按相同的查詢條件重新讀取以前檢索過的數據,卻發現其他事務插入了滿足其查詢條件的新數據,這種現象就稱為“幻讀”。
即事務A 執行兩次 select 操作得到不同的數據集,即 select 1 得到 10 條記錄,select 2 得到 11 條記錄。
這其實并不是幻讀,這是不可重復讀的一種,只會在 R-U R-C 級別下出現,而在 mysql 默認的 RR 隔離級別是不會出現的(下面會舉例推翻)。
然而,我終于在茫茫文章中,找到了相對正確的解釋:
幻讀,并不是說兩次讀取獲取的結果集不同,幻讀側重的方面是某一次的 select 操作得到的結果所表征的數據狀態無法支撐后續的業務操作。
更為具體一些:select 某記錄是否存在,不存在,準備插入此記錄,但執行 insert 時發現此記錄已存在,無法插入,此時就發生了幻讀。
推翻錯誤的解釋
事務隔離級別
mysql 有四級事務隔離級別 每個級別都有字符或數字編號
讀未提交 READ-UNCOMMITTED | 0:存在臟讀,不可重復讀,幻讀的問題
讀已提交 READ-COMMITTED | 1:解決臟讀的問題,存在不可重復讀,幻讀的問題
可重復讀 REPEATABLE-READ | 2:解決臟讀,不可重復讀的問題,存在幻讀的問題,默認隔離級別,使用 MMVC機制 實現可重復讀
序列化 SERIALIZABLE | 3:解決臟讀,不可重復讀,幻讀,可保證事務安全,但完全串行執行,性能最低
幻讀會在 RU / RC / RR 級別下出現,SERIALIZABLE 則杜絕了幻讀,但 RU / RC 下還會存在臟讀,不可重復讀,故我們就以 RR 級別來研究幻讀,排除其他干擾。
舉例推翻
建表a,id列自增主鍵,name列唯一索引。
CREATE TABLE a (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY UIDX_NAME (name)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
準備初始數據:
INSERT INTO a VALUES(NULL,'a'),(NULL,'b');
MySQL隔離級別設置為RR(默認),準備兩個事務AB。然后依次執行:
- 開始事務A,執行查詢語句:
START TRANSACTION;
SELECT * FROM a WHERE name BETWEEN 'a' AND 'z';
+----+------+
| id | name |
+----+------+
| 1 | a |
| 2 | b |
+----+------+
2 rows in set
- 開啟事務B,執行插入語句,并提交
START TRANSACTION;
INSERT INTO a VALUES(NULL,'c'),(NULL,'d');
COMMIT;
- 事務A再次執行查詢語句:
SELECT * FROM a WHERE name BETWEEN 'a' AND 'z';
+----+------+
| id | name |
+----+------+
| 1 | a |
| 2 | b |
+----+------+
2 rows in set
小結
可以看到,在RR級別下,所謂的"幻讀"并沒有出現。而SQL-92標準中定義的RR級別是沒法解決幻讀的,這就是矛盾點所在。如果是所謂的"幻讀",事務A應該讀到abcd四條數據。出現這種情況有兩種可能:
- MySQL在RR級別解決了幻讀。
- 這不是真正的幻讀。
查閱MySQL官方文檔,并沒有某一段文字說明其在RR級別解決了幻讀問題。
https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html
所以說,其實這并不是“幻讀”的范疇,這仍然屬于RR級別所解決的,不可重復讀范疇。
解釋
我們能確定的是,RR級別解決了不可重復讀的問題。
那么為什么說上述例子屬于不可重復讀范疇呢?我們得從解決不可重復讀問題的原理MVCC講起。
MVCC
MySQL InnoDB存儲引擎,實現的是基于多版本的并發控制協議——MVCC (Multi-Version Concurrency Control) (注:與MVCC相對的,是基于鎖的并發控制,Lock-Based Concurrency Control)。MVCC最大的好處,相信也是耳熟能詳:讀不加鎖,讀寫不沖突。在讀多寫少的OLTP應用中,讀寫不沖突是非常重要的,極大的增加了系統的并發性能,這也是為什么現階段,幾乎所有的RDBMS,都支持了MVCC。
在MVCC并發控制中,讀操作可以分成兩類:快照讀 (snapshot read)與當前讀 (current read)。快照讀,讀取的是記錄的可見版本 (有可能是歷史版本),不用加鎖。當前讀,讀取的是記錄的最新版本,并且,當前讀返回的記錄,都會加上鎖,保證其他事務不會再并發修改這條記錄。
快照讀VS當前讀
在一個支持MVCC并發控制的系統中,哪些讀操作是快照讀?哪些操作又是當前讀呢?以MySQL InnoDB為例:
快照讀:簡單的select操作,屬于快照讀,不加鎖。
select * from table where ?;
當前讀:特殊的讀操作,插入/更新/刪除操作,屬于當前讀,需要加鎖。
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
所有以上的語句,都屬于當前讀,讀取記錄的最新版本。并且,讀取之后,還需要保證其他并發事務不能修改當前記錄,對讀取記錄加鎖。其中,除了第一條語句,對讀取記錄加S鎖 (共享鎖)外,其他的操作,都加的是X鎖 (排它鎖)。
為什么將 插入/更新/刪除 操作,都歸為當前讀?
一個Update操作的具體流程。當Update SQL被發給MySQL后,MySQL Server會根據where條件,讀取第一條滿足條件的記錄,然后InnoDB引擎會將第一條記錄返回,并加鎖 (current read)。待MySQL Server收到這條加鎖的記錄之后,會再發起一個Update請求,更新這條記錄。一條記錄操作完成,再讀取下一條記錄,直至沒有滿足條件的記錄為止。因此,Update操作內部,就包含了一個當前讀。同理,Delete操作也一樣。Insert操作會稍微有些不同,簡單來說,就是Insert操作可能會觸發Unique Key的沖突檢查,也會進行一個當前讀。
注:針對一條當前讀的SQL語句,InnoDB與MySQL Server的交互,是一條一條進行的,因此,加鎖也是一條一條進行的。先對一條滿足條件的記錄加鎖,返回給MySQL Server,做一些DML操作;然后在讀取下一條加鎖,直至讀取完畢。
總結
所以說,在上述例子中,事務A第二次讀取(快照讀)的是記錄的歷史版本。而不是最新的版本——事務B插入新紀錄后的abcd四條。這屬于不可重復讀的范疇。
有些博文錯誤
的把這種情況歸為“幻讀”。甚至還有的說MySQL的RR級別解決了幻讀。在此糾錯。
參考:
https://segmentfault.com/a/1190000016566788?utm_source=tag-newest
http://hedengcheng.com/?p=771