MySQL MVVC

什麼是MVVC

 


MVVC (Multi-Version Concurrency Control) (注:與MVCC相對的,是基於鎖的併發控制,Lock-Based Concurrency Control)是一種基於多版本的併發控制協議,只有在InnoDB引擎下存在。MVCC是爲了實現事務的隔離性,通過版本號,避免同一數據在不同事務間的競爭,你可以把它當成基於多版本號的一種樂觀鎖。當然,這種樂觀鎖只在事務級別提交讀和可重複讀有效。MVCC最大的好處,相信也是耳熟能詳:讀不加鎖,讀寫不衝突。在讀多寫少的OLTP應用中,讀寫不衝突是非常重要的,極大的增加了系統的併發性能。

不僅是MySQL,包括Oracle,PostgreSQL等其他數據庫系統也都實現了MVCC,但各自的實現機制不盡相同,因爲MVCC沒有一個統一的實現標準。

可以認爲MVCC是行級鎖的一個變種,但是它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現機制有所不同,但大都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。

MVCC的實現方式有多種,典型的有樂觀(optimistic)併發控制 和 悲觀(pessimistic)併發控制。

MVCC只在 READ COMMITTED 和 REPEATABLE READ 兩個隔離級別下工作。其他兩個隔離級別夠和MVCC不兼容,因爲 READ UNCOMMITTED 總是讀取最新的數據行,而不是符合當前事務版本的數據行。而 SERIALIZABLE 則會對所有讀取的行都加鎖。

MVVC的實現機制
InnoDB在每行數據都增加三個隱藏字段,一個唯一行號,一個記錄創建的版本號,一個記錄回滾的版本號。

 

在多版本併發控制中,爲了保證數據操作在多線程過程中,保證事務隔離的機制,降低鎖競爭的壓力,保證較高的併發量。在每開啓一個事務時,會生成一個事務的版本號,被操作的數據會生成一條新的數據行(臨時),但是在提交前對其他事務是不可見的,對於數據的更新(包括增刪改)操作成功,會將這個版本號更新到數據的行中,事務提交成功,將新的版本號更新到此數據行中,這樣保證了每個事務操作的數據,都是互不影響的,也不存在鎖的問題。

 

undo-log
undo log是爲回滾而用,具體內容就是copy事務前的數據庫內容(行)到undo buffer,在適合的時間把undo buffer中的內容刷新到磁盤。undo buffer與redo buffer一樣,也是環形緩衝,但當緩衝滿的時候,undo buffer中的內容會也會被刷新到磁盤;與redo log不同的是,磁盤上不存在單獨的undo log文件,所有的undo log均存放在主ibd數據文件中(表空間),即使客戶端設置了每表一個數據文件也是如此。

InnoDB存儲引擎在數據庫每行數據的後面添加了三個字段
6字節的事務ID(DB_TRX_ID)字段:用來標識最近一次對本行記錄做修改(insert|update)的事務的標識符,即最後一次修改(insert|update)本行記錄的事務id。至於delete操作,在innodb看來也不過是一次update操作,更新行中的一個特殊位將行表示爲deleted,並非真正刪除。
7字節的回滾指針(DB_ROLL_PTR)字段:指寫入回滾段(rollback segment)的 undo log record (撤銷日誌記錄記錄)。如果一行記錄被更新, 則 undo log record 包含 ‘重建該行記錄被更新之前內容’ 所必須的信息。
6字節的DB_ROW_ID字段:包含一個隨着新行插入而單調遞增的行ID,當由innodb自動產生聚集索引時,聚集索引會包括這個行ID的值,否則這個行ID不會出現在任何索引中。
結合聚簇索引的相關知識點,如果表中沒有主鍵或合適的唯一索引,也就是無法生成聚簇索引的時候,InnoDB會幫我們自動生成聚集索引,但聚簇索引會使用DB_ROW_ID的值來作爲主鍵;如果有主鍵或者合適的唯一索引,那麼聚簇索引中也就不會包含 DB_ROW_ID了 。

Read View和快照Snapshot
事務快照是用來存儲數據庫的事務運行情況。一個事務快照的創建過程可以概括爲:

查看當前所有的未提交併活躍的事務,存儲在數組中
選取未提交併活躍的事務中最小的XID,記錄在快照的xmin中
選取所有已提交事務中最大的XID,加1後記錄在xmax中
Read View (主要是用來做可見性判斷的):創建一個新事務時,copy一份當前系統中的活躍事務列表。意思是,當前不應該被本事務看到的其他事務id列表。

對於Read View快照的生成時機,也非常關鍵,正是因爲生成時機的不同,造成了RC,RR兩種隔離級別的不同可見性;

在innodb中(默認repeatable read級別),事務在begin/start transaction之後的第一條select讀操作後,會創建一個快照(Read View),將當前系統中活躍的其他事務記錄記錄起來
在innodb中(默認repeatable committed級別),事務中每條select語句都會創建一個快照(Read View)
RC是語句級多版本(事務的多條只讀語句,創建不同的ReadView,代價更高),RR是事務級多版本(一個ReadView);

read committed 總是讀最新一份快照數據,而repeatable read 讀事務開始時的行數據版本。

read Commited隔離級別判斷算法在每次語句執行的過程中,都關閉read_view, 重新創建當前的一份新的read_view。

read view中事務id T_min~T_max,當前事務T1。
...執行sql,創建一份最新的read_view;
...T1<T_min,說明T1事務比較早,該行對當前事務T1可見。
...T1 > T_max,說明T1比較晚,該行對當前事務不可見,根據DB_ROLL_PTR找到上一個判斷再次判斷。
...T_min <= T1 <= T_max,如果read_view中有該事務,則不可見,找上一個版本。如果不在則可見(在read commited下)。

repeatable read各級離別下判斷算法:創建事務trx結構的時候,就生成了當前的global read view。
...trx_id_1< trx_id_min那麼表明該行記錄所在的事務已經在本次新事務創建之前就提交了,所以該行記錄的當前值是可見的。
...trx_id_1>trx_id_max的話,那麼表明該行記錄所在的事務在本次新事務創建之後纔開啓,所以該行記錄的當前值不可見。通過DB_ROLL_PTR找到上一版數據判斷
...trx_id_min<=trx_id_<=trx_id_max, 那麼表明該行記錄所在事務在本次新事務創建的時候處於活動狀態,從trx_id_min到trx_id_max進行遍歷,如果trx_id_1等於他們之中的某個事務id的話,那麼不可見。通過DB_ROLL_PTR找到上一版數據判斷`
實驗1:

session A

session B

mysql> set tx_isolation='repeatable-read';

mysql> set tx_isolation='repeatable-read';

 

mysql> select * from t1;

Empty set (0.01 sec)

mysql> start transaction;

 

 

mysql> insert into t1(c1,c2) values(1,1);

mysql> select * from t1;

+----+------+

| c1 | c2   |

+----+------+

|  1 |    1 |

+----+------+

1 row in set (0.00 sec)

 

實驗2:

mysql> set tx_isolation='repeatable-read';

mysql> set tx_isolation='repeatable-read';

 

mysql> select * from t1;

Empty set (0.01 sec)

mysql> start transaction with consistent snapshot;

 

 

mysql> insert into t1(c1,c2) values(1,1);

mysql> select * from t1;

Empty set (0.00 sec)

 

上面兩個實驗很好的說明了 start transaction 和 start tansaction with consistent snapshot的區別。第一個實驗說明,start transaction執行之後,事務並沒有開始,所以insert發生在session A的事務開始之前,所以可以讀到session B插入的值。第二個實驗說明,start transaction with consistent snapshot已經開始了事務,所以insert語句發生在事務開始之後,所以讀不到insert的數據。

所以事務開始時間點,分爲兩種情況:

START TRANSACTION 時,是第一條語句的執行時間點,就是事務開始的時間點,第一條select語句建立一致性讀的snapshot;
START TRANSACTION  WITH consistent snapshot 時,則是立即建立本事務的一致性讀snapshot,當然也開始事務了;
參考:http://www.cnblogs.com/digdeep/p/4947694.html

一致性讀肯定是讀取在某個時間點已經提交了的數據,有個特例:本事務中修改的數據,即使未提交的數據也可以在本事務的後面部分讀取到。

MVVC下的CRUD
SELECT:
  當隔離級別是REPEATABLE READ時select操作,InnoDB必須每行數據來保證它符合兩個條件:

InnoDB必須找到一個行的版本,它至少要和事務的版本一樣老(也即它的版本號不大於事務的版本號)。這保證了不管是事務開始之前,或者事務創建時,或者修改了這行數據的時候,這行數據是存在的。
這行數據的刪除版本必須是未定義的或者比事務版本要大。這可以保證在事務開始之前這行數據沒有被刪除。
符合這兩個條件的行可能會被當作查詢結果而返回。

INSERT:InnoDB爲這個新行記錄當前的系統版本號。
DELETE:InnoDB將當前的系統版本號設置爲這一行的刪除ID。
UPDATE:InnoDB會寫一個這行數據的新拷貝,這個拷貝的版本爲當前的系統版本號。它同時也會將這個版本號寫到舊行的刪除版本里。

這種額外的記錄所帶來的結果就是對於大多數查詢來說根本就不需要獲得一個鎖。只是簡單地以最快的速度來讀取數據,確保只選擇符合條件的行。這個方案的缺點在於存儲引擎必須爲每一行存儲更多的數據,做更多的檢查工作,處理更多的善後操作。

MVCC只工作在REPEATABLE READ和READ COMMITED隔離級別下。READ UNCOMMITED不是MVCC兼容的,因爲查詢不能找到適合他們事務版本的行版本;它們每次都只能讀到最新的版本。SERIABLABLE也不與MVCC兼容,因爲讀操作會鎖定他們返回的每一行數據。

當前讀和快照讀
MySQL的InnoDB存儲引擎默認事務隔離級別是RR(可重複讀), 是通過 “行排他鎖+MVCC” 一起實現的,不僅可以保證可重複讀,還可以部分防止幻讀,而非完全防止;

爲什麼是部分防止幻讀,而不是完全防止?

效果: 在如果事務B在事務A執行中,insert了一條數據並提交,事務A再次查詢,雖然讀取的是undo中的舊版本數據(防止了部分幻讀),但是事務A中執行update或者delete都是可以成功的。(參考:MySQL 讀提交和重複讀隔離級別實驗 實驗三)

因爲在innodb中的操作可以分爲當前讀(current read)和快照讀(snapshot read):

快照讀:讀取的是快照版本,也就是歷史版本
簡單的select操作(當然不包括 select … lock in share mode, select … for update)

當前讀:讀取的是最新版本
UPDATE、DELETE、INSERT、SELECT …  LOCK IN SHARE MODE、SELECT … FOR UPDATE是當前讀。

在RR級別下,快照讀是通過MVVC(多版本控制)和undo log來實現的,當前讀是通過加record lock(記錄鎖)和gap lock(間隙鎖)來實現的。

參考:https://www.jianshu.com/p/adb15359d924
————————————————
版權聲明:本文爲CSDN博主「huaishu」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/huaishu/article/details/89924250

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