最近在網(wǎng)上看了不少mysql鎖的文章,不少文章都提到InnoDB的RR隔離級別(Repeatable Read)無法解決幻讀的問題。對此問題作者親自做了一些實驗,將實驗結(jié)論記錄在此。
本次實驗的mysql版本為5.7.22。
理解本次實驗所需具備的基礎(chǔ)知識
此篇文章的重點在于通過實驗的形式解釋清楚InnoDB的RR隔離級別是否解決了幻讀問題。所以文中將不會對一些相關(guān)的概念進行解釋,默認讀者已經(jīng)具備相關(guān)知識。如果讀者對于以下的知識點不甚清楚,最好自行查閱相關(guān)資料,理解清楚之后再閱讀接下來的實驗內(nèi)容,以免造成困惑。
進行此次實驗需要具備的知識點(包括但不限于):
- InnoDB的事務(wù)
- InnoDB的MVVC(Multi-Version Concurrency Control,多版本并發(fā)控制)
- InnoDB存儲引擎的四種隔離級別
- 臟讀、可重復讀和幻讀的概念
- InnoDB的鎖機制(共享鎖S,排它鎖X,表鎖,行鎖)
- next-key lock和gap lock
測試用數(shù)據(jù)表
創(chuàng)建表結(jié)構(gòu):
CREATE TABLE tmp_table (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255));
創(chuàng)建兩條數(shù)據(jù):
INSERT INTO tmp_table (name) values ('andy'),('jerry');
最終的表數(shù)據(jù)如下:
| id | name |
|---|---|
| 1 | andy |
| 2 | jerry |
進行實驗
打開兩個終端,連上mysql,分別啟動事務(wù)a和事務(wù)b。
在事務(wù)a和事務(wù)b上面分別執(zhí)行如下命令:
| 事務(wù)a | 事務(wù)b |
|---|---|
| start transaction; | start transaction; |
| insert into tmp_table (name) values ('newa'); | -- |
| -- | select * from tmp_table; |
| select * from tmp_table; | -- |
查詢出來的結(jié)果如下:
事務(wù)a:
| id | name |
|---|---|
| 1 | andy |
| 2 | jerry |
| 3 | newa |
事務(wù)b:
| id | name |
|---|---|
| 1 | andy |
| 2 | jerry |
很明顯事務(wù)b沒有查詢到事務(wù)a未提交的新插入數(shù)據(jù)。原因也很簡單,因為普通的select語句是快照讀,而事務(wù)b啟動時,它的快照數(shù)據(jù)就已經(jīng)被版本鎖定了。
如果事務(wù)b進行當前讀是否能夠讀取到事務(wù)a未提交的新插入數(shù)據(jù)呢?
那么我們在事務(wù)b里面執(zhí)行如下命令來看看執(zhí)行結(jié)果:
select * from tmp_table lock in share mode;
執(zhí)行完成之后我們發(fā)現(xiàn)事務(wù)b此時會block住,原因是事務(wù)a的insert語句排它鎖住了id為3的新插入數(shù)據(jù),而事務(wù)b想請求所有行的共享鎖,肯定是需要等待的。
那么此時事務(wù)b當前讀id為1或2的數(shù)據(jù)(非事務(wù)a新插入數(shù)據(jù))是否可行呢?
結(jié)論是可行的,因為tmp_table存在唯一鍵,且事務(wù)a的insert語句只是鎖住了id為3的行。所以其他事務(wù)獲取其他行的共享鎖是可行的。讀者可以自行測試,這里就不做演示了。
事務(wù)a提交之后,事務(wù)b此時能否讀取到事務(wù)a新插入的數(shù)據(jù)呢?
事務(wù)a和事務(wù)b執(zhí)行如下命令:
| 事務(wù)a | 事務(wù)b |
|---|---|
| commit; | -- |
| -- | select * from tmp_table; |
事務(wù)b打印的結(jié)果:
| id | name |
|---|---|
| 1 | andy |
| 2 | jerry |
還是一樣,因為普通select是快照讀,事務(wù)b還是讀取到的是快照數(shù)據(jù),所以不包含事務(wù)a提交之后的新數(shù)據(jù)。
如果此時事務(wù)b使用當前讀,能否獲取到事務(wù)a已提交的新插入數(shù)據(jù)呢?
讓我們在事務(wù)b下面使用共享鎖查看當前版本數(shù)據(jù):
select * from tmp_table lock in share mode;
結(jié)果如下:
| id | name |
|---|---|
| 1 | andy |
| 2 | jerry |
| 3 | newa |
可以查詢到事務(wù)a已提交的新數(shù)據(jù),所以此時使用當前讀就產(chǎn)生了幻讀。
另一種情況的幻讀
還有另一種情況也會產(chǎn)生幻讀,并且只需要執(zhí)行普通的select語句。下面請看演示。
在事務(wù)b下面執(zhí)行如下兩條語句:
update tmp_table set name='bbb' where id = 3;
select * from tmp_table;
第一條命令使用update更新了事務(wù)a已提交的新數(shù)據(jù),第二條命令通過普通的select語句查看快照數(shù)據(jù)。
打印結(jié)果如下:
| id | name |
|---|---|
| 1 | andy |
| 2 | jerry |
| 3 | bbb |
可以看到事務(wù)a已提交的新數(shù)據(jù)被事務(wù)b使用update語句更新了,并且通過普通的select語句給查詢出來了,很顯然,出現(xiàn)了幻讀。
結(jié)論
所以說InnoDB的RR隔離級別沒有或者解決了幻讀問題都不太準確。應(yīng)該說它并沒有完全解決幻讀的問題。
如果在同一個事務(wù)里面,只是總是執(zhí)行普通的select快照讀,是不會產(chǎn)生幻讀的。
但是如果在這個事務(wù)里面通過當前讀或者先更新然后快照讀的形式來讀取數(shù)據(jù),就會產(chǎn)生幻讀。