透徹解讀mysql的可重複讀、幻讀及實現原理

目錄

一、事務的隔離級別

二、mysql怎麼實現的可重複讀

舉例說明MVCC的實現

MVCC邏輯流程-插入

MVCC邏輯流程-刪除

MVCC邏輯流程-修改

MVCC邏輯流程-查詢

三、幻讀

快照讀和當前讀

四、如何解決幻讀


事務隔離級別有四種,mysql默認使用的是可重複讀,mysql是怎麼實現可重複讀的?爲什麼會出現幻讀?是否解決了幻讀的問題?

一、事務的隔離級別

Read Uncommitted(未提交讀)
在該隔離級別,所有事務都可以看到其他未提交事務的執行結果。讀取未提交的數據,也被稱之爲髒讀(Dirty Read)。該級別用的很少。

Read Committed(提交讀)
這是大多數數據庫系統的默認隔離級別(但不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看見已經提交事務所做的改變,換句話說就是事務提交之前對其餘事務不可見。這種隔離級別也支持不可重複讀(Nonrepeatable Read),因爲同一事務的其他實例在該實例處理其間可能會有新的commit,所以同一select查詢可能返回不同結果。

Repeatable Read(可重複讀)
這是MySQL的默認事務隔離級別,它確保同一事務的多個實例在併發讀取數據時,會看到同樣的數據行。不過理論上,這會導致另一個棘手的問題:幻讀 (Phantom Read)。簡單的說,幻讀指當用戶讀取某一範圍的數據行時,另一個事務又在該範圍內插入了新行,當用戶再讀取該範圍的數據行時,會發現有新的“幻影” 行。InnoDB和Falcon存儲引擎通過多版本併發控制(MVCC,Multiversion Concurrency Control)機制解決了該問題(mysql徹底解決了幻讀問題?請往下看)

Serializable(可串行化)
這是最高的隔離級別,它強制事務都是串行執行的,使之不可能相互衝突,從而解決幻讀問題。換言之,它是在每個讀的數據行上加上共享鎖。在這個級別,可能導致大量的超時現象和鎖競爭。

事務隔離級別 髒讀 不可重複讀 幻讀
讀未提交(read-uncommitted)
不可重複讀(read-committed)
可重複讀(repeatable-read)
串行化(serializable)

在MySQL的衆多存儲引擎中,只有InnoDB支持事務,所有這裏說的事務隔離級別指的是InnoDB下的事務隔離級別。

二、mysql怎麼實現的可重複讀

MVCC多版本併發控制(Multi-Version Concurrency Control)是MySQL中基於樂觀鎖理論實現隔離級別的方式,用於實現讀已提交和可重複讀取隔離級別。

在《高性能MySQL》中對MVCC的解釋如下

舉例說明MVCC的實現

新建一張表test_zq如下

id test_id DB_TRX_ID DB_ROLL_PT

MVCC邏輯流程-插入

在插入數據的時候,假設系統的全局事務ID從1開始,以下SQL語句執行分析參考註釋信息:

begin;-- 獲取到全局事務ID
insert into `test_zq` (`id`, `test_id`) values('5','68');
insert into `test_zq` (`id`, `test_id`) values('6','78');
commit;-- 提交事務
複製代碼

當執行完以上SQL語句之後,表格中的內容會變成:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 NULL
6 78 1 NULL

可以看到,插入的過程中會把全局事務ID記錄到列 DB_TRX_ID 中去

MVCC邏輯流程-刪除

對上述表格做刪除邏輯,執行以下SQL語句(假設獲取到的事務邏輯ID爲 3)

begin;--獲得全局事務ID = 3
delete test_zq where id = 6;
commit;
複製代碼

執行完上述SQL之後數據並沒有被真正刪除,而是對刪除版本號做改變,如下所示:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 NULL
6 78 1 3

MVCC邏輯流程-修改

修改邏輯和刪除邏輯有點相似,修改數據的時候 會先複製一條當前記錄行數據,同事標記這條數據的數據行版本號爲當前是事務版本號,最後把原來的數據行的刪除版本號標記爲當前是事務。

執行以下SQL語句:

begin;-- 獲取全局系統事務ID 假設爲 10
update test_zq set test_id = 22 where id = 5;
commit;
複製代碼

執行後表格實際數據應該是:

id test_id DB_TRX_ID DB_ROLL_PT
5 68 1 10
6 78 1 3
5 22 10 NULL

MVCC邏輯流程-查詢

此時,數據查詢規則如下:

  • 查找數據行版本號早於當前事務版本號的數據行記錄

    也就是說,數據行的版本號要小於或等於當前是事務的系統版本號,這樣也就確保了讀取到的數據是當前事務開始前已經存在的數據,或者是自身事務改變過的數據

  • 查找刪除版本號要麼爲NULL,要麼大於當前事務版本號的記錄

    這樣確保查詢出來的數據行記錄在事務開啓之前沒有被刪除

根據上述規則,我們繼續以上張表格爲例,對此做查詢操作

begin;-- 假設拿到的系統事務ID爲 12
select * from test_zq;
commit;
複製代碼

執行結果應該是:

id test_id DB_TRX_ID DB_ROLL_PT
6 22 10 NULL

這樣,同一個事務中,就實現了可重複讀。

三、幻讀

什麼時幻讀,如下:

姑且把左邊的事務命名爲事務A,右邊的命名爲事務B。
事務B執行後,在事務A中查詢沒有查到B添加的數據行,這就是可重複讀。
但是,在事務A執行了update後,再查詢時就查到了事務A中添加的數據,這就是幻讀。
這種結果告訴我們其實在MySQL可重複讀的隔離級別中並不是完全解決了幻讀的問題,而是解決了讀數據情況下的幻讀問題。而對於修改的操作依舊存在幻讀問題,就是說MVCC對於幻讀的解決是不徹底的。

快照讀和當前讀

出現了上面的情況我們需要知道爲什麼會出現這種情況。在查閱了一些資料後發現在RR級別中,通過MVCC機制,雖然讓數據變得可重複讀,但我們讀到的數據可能是歷史數據,不是數據庫最新的數據。這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取數據庫最新版本數據的方式,叫當前讀 (current read)。

select 快照讀

當執行select操作是innodb默認會執行快照讀,會記錄下這次select後的結果,之後select 的時候就會返回這次快照的數據,即使其他事務提交了不會影響當前select的數據,這就實現了可重複讀了。快照的生成當在第一次執行select的時候,也就是說假設當A開啓了事務,然後沒有執行任何操作,這時候B insert了一條數據然後commit,這時候A執行 select,那麼返回的數據中就會有B添加的那條數據。之後無論再有其他事務commit都沒有關係,因爲快照已經生成了,後面的select都是根據快照來的。

當前讀

對於會對數據修改的操作(update、insert、delete)都是採用當前讀的模式。在執行這幾個操作時會讀取最新的版本號記錄,寫操作後把版本號改爲了當前事務的版本號,所以即使是別的事務提交的數據也可以查詢到。假設要update一條記錄,但是在另一個事務中已經delete掉這條數據並且commit了,如果update就會產生衝突,所以在update的時候需要知道最新的數據。也正是因爲這樣所以才導致幻讀。

四、如何解決幻讀

很明顯可重複讀的隔離級別沒有辦法徹底的解決幻讀的問題,如果我們的項目中需要解決幻讀的話也有兩個辦法:

  • 使用串行化讀的隔離級別
  • MVCC+next-key locks:next-key locks由record locks(索引加鎖) 和 gap locks(間隙鎖,每次鎖住的不光是需要使用的數據,還會鎖住這些數據附近的數據),next-key lock 會鎖定範圍和自身行,比如select...where id<6,鎖定的是小於6的行和等於6的行

Next-Key Lock即在事務中select時使用如果方法加鎖,這樣在另一個事務對範圍內的數據進行修改時就會阻塞:

select * from table where id<6 lock in share mode;--共享鎖
select * from table where id<6 for update;--排他鎖

實際上很多的項目中是不會使用到上面的兩種方法的,串行化讀的性能太差,而且其實幻讀很多時候是我們完全可以接受的。

關於next-key locks請參考https://www.cnblogs.com/zhoujinyi/p/3435982.html

參考文章:

https://juejin.im/post/5c68a4056fb9a049e063e0ab

https://zhuanlan.zhihu.com/p/35500144

https://www.jianshu.com/p/69fd2ca17cfd

《高性能MySQL》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章