mysql的MVCC

張喜碩學長之前講過一篇MySQL RR 與 鎖,在本週又看到了RR的問題,裏面提到了RR是經過MVCC實現的,可是本身對此卻沒什麼印象,翻了翻學長的博客也沒講過,就學習一下,作個記錄。git

MVCC

MVCC 即多版本併發控制技術,簡單的理解就是一份數據保存了多份。github

用於多事務環境下,對數據讀寫在不加讀寫鎖的狀況下實現互不干擾,從而實現數據庫的隔離性,在事務隔離級別爲Read Commit 和 Repeatable read中使用到。sql

在InnoDB中,MVCC實際上是經過undo log來實現的,但使用undo log解釋起來較爲複雜,因此廣泛的解釋是:每行記錄的後面保存了兩個隱藏的列,DB_TRX_ID(數據行的版本號)DB_ROLL_PT(刪除版本號),這兩列保存的是系統版本號,每開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會做爲事務的版本號,用來和查詢到的每行記錄的版本號進行比較。下面看看進行不一樣的操做時(如下內容取RR隔離級別,固然RC也是同理,只不過select的選定範圍不一樣),InnoDB的行爲:數據庫

  • SELECT
    InnoDB會根據如下兩個條件檢查每行記錄:segmentfault

    1. InnoDB只查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣能夠確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。
    2. 行的刪除版本要麼未定義,要麼大於當前事務版本號。這能夠確保事務讀取到的行,在事務開始以前未被刪除。
  • INSERT

    InnoDB爲新插入的每一行保存當前系統版本號做爲數據行版本號。併發

  • DELETE

    InnoDB爲刪除的每一行保存當前系統版本號做爲行刪除版本號。post

  • UPDATE

    InnoDB插入一條新記錄,保存當前系統版本號做爲數據行版本號,同時保存當前系統版本號到原來的行做爲刪除版本號。性能

保存這兩個額外系統版本號,使大多數讀操做均可以不用加鎖。這樣設計使得讀數據操做很簡單,性能很好,而且也能保證只會讀取到符合標準的行,不足之處是每行記錄都須要額外的存儲空間,須要作更多的行檢查工做,以及一些額外的維護工做。學習

光看概念確定仍是看的不太明白的,咱們用一個例子來展現一下測試

例子

先建立一個用戶表

create table user( 
id int primary key auto_increment, 
name varchar(20));

打開navicat,新建一個查詢,執行如下sql

begin; # 開始一個新的事務, 事務的版本號爲1
insert into user values(NULL,'zhangsan');
insert into user values(NULL,'lisi');
commit;

此時數據庫中的數據應該是這樣,由於新插入的每一行會保存當前系統版本號做爲數據行版本號

Id name DB_TRX_ID(數據行版本號) DB_ROLL_PT(刪除版本號)
1 zhangsan 1 null
2 lisi 1 null

此時, 咱們打開一個新的查詢, 把它稱做Query1

begin; # 開始一個新的事務,事務版本號爲2
select * from user; # 1 
select * from user; # 2
commit;

此時,執行Query1中的1
image.png

咱們再打開一個查詢, 把它稱做Query2

begin; # 開始一個新的事務,事務版本號爲3
update user set name = 'yuzhi' where id = 1;
commit;

執行Query2,以後咱們在執行Query1的2

image.png

結果和Query1的1查詢到的是同樣的,這符合咱們的預期,由於此時數據庫中的數據應該是這樣

Id name DB_TRX_ID(數據行的版本號) DB_ROLL_PT(刪除版本號)
1 zhangsan 1 3
1 yunzhi 3 null
2 lisi 1 null

Query1只能查詢數據行版本號小於等於當前事務版本號或未定義且刪除版本號大於當前事務版本號的。

刪除操做同理,再也不演示,咱們對Query進行commit。

MVCC與幻讀

上面的例子證實了MVCC可以實現可重複讀,可是MVCC是否可以避免幻讀呢?咱們繼續看。

咱們新建一個查詢,叫作Query3

begin; # 開啓一個新的事務,事務版本號4 
select * from user; # 1
select * from user; # 2
update user set name='yunzhi'; # 3
select * from user; 
commit;

Query3的1,此時數據庫中的數據應該是這樣(第一條記錄由於事務1已關閉,因此被清除了)

Id name DB_TRX_ID(數據行的版本號) DB_ROLL_PT(刪除版本號)
1 yunzhi 3 null
2 lisi 1 null

新建一個查詢Query4,

begin; # 開啓一個新的事務, 事務版本號爲5
insert into user values(NULL,'wangwu');
commit;

執行Query4, 此時再執行Query3的2, 查詢出來的結果爲

image.png

符合預期, 由於此時數據庫中的數據應該是這樣

Id name DB_TRX_ID(數據行的版本號) DB_ROLL_PT(刪除版本號)
1 yunzhi 3 null
2 lisi 1 null
3 wangwu 5 null

而進行查詢的事務id爲4

咱們接着執行Query3的3和4

image.png三條數據全都被修改了, 而且被查出來了!!!

快照讀和當前讀

在查閱了一些資料後發如今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的時候須要知道最新的數據。也正是由於這樣因此才致使上面咱們測試的那種狀況。

select的當前讀須要手動的加鎖:

select * from table where ? lock in share mode;
select * from table where ? for update;

同時update之後會把之前的標記爲刪除,而增長一條數據,因此此時數據庫中的數據應該是這樣

Id name DB_TRX_ID(數據行的版本號) DB_ROLL_PT(刪除版本號)
1 yunzhi 3 4
1 yunzhi 4 null
2 lisi 1 4
2 yunzhi 4 null
3 wangwu 5 4
3 yunzhi 4 null

這也就解釋了爲何後續的select能把全部數據查詢出來。

小結

MySQL可重複讀的隔離級別中並非徹底解決了幻讀的問題,而是解決了讀數據狀況下的幻讀問題。而對於修改的操做依舊存在幻讀問題,就是說MVCC對於幻讀的解決是不完全的。

如何解決幻讀

有兩個辦法:

  • 使用串行化讀的隔離級別
  • MVCC+next-key locks:next-key locks由record locks(索引加鎖) 和 gap locks(間隙鎖,每次鎖住的不光是須要使用的數據,還會鎖住這些數據附近的數據)

一個注意事項

若是隻是執行 begin語句實際上並不會開啓一個事務。
對數據進行了增刪改查等操做後纔會開啓一個事務。

版權聲明

本文做者: 河北工業大學夢雲智開發團隊 - 李宜衡

參考文章

MYSQL MVCC實現原理

MVCC 能解決幻讀嗎?

MySQL的可重複讀級別能解決幻讀嗎

相關文章
相關標籤/搜索