張喜碩學長之前講過一篇MySQL RR 與 鎖,在本週又看到了RR的問題,裏面提到了RR是經過MVCC實現的,可是本身對此卻沒什麼印象,翻了翻學長的博客也沒講過,就學習一下,作個記錄。git
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
InnoDB爲新插入的每一行保存當前系統版本號做爲數據行版本號。併發
InnoDB爲刪除的每一行保存當前系統版本號做爲行刪除版本號。post
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
咱們再打開一個查詢, 把它稱做Query2
begin; # 開始一個新的事務,事務版本號爲3 update user set name = 'yuzhi' where id = 1; commit;
執行Query2,以後咱們在執行Query1的2
結果和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是否可以避免幻讀呢?咱們繼續看。
咱們新建一個查詢,叫作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, 查詢出來的結果爲
符合預期, 由於此時數據庫中的數據應該是這樣
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
三條數據全都被修改了, 而且被查出來了!!!
在查閱了一些資料後發如今RR級別中,經過MVCC機制,雖然讓數據變得可重複讀,但咱們讀到的數據多是歷史數據,不是數據庫最新的數據。這種讀取歷史數據的方式,咱們叫它快照讀 (snapshot read),而讀取數據庫最新版本數據的方式,叫當前讀 (current read)。
當執行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對於幻讀的解決是不完全的。
有兩個辦法:
若是隻是執行begin
語句實際上並不會開啓一個事務。
對數據進行了增刪改查等操做後纔會開啓一個事務。
本文做者: 河北工業大學夢雲智開發團隊 - 李宜衡