MySQL MVVC多版本並發控制的實現詳解

一、概述

MVCC(Multiversion Concurrency Control),多版本並發控制。它和undo log中的版本鏈息息相關,MVVC通過數據行的多個版本來實現數據庫的並發控制。

簡單的說就是當前事務查詢另一個事務正在更改的行(如果此時讀取就會發生臟讀),不用加鎖等待,而是讀取該數據的歷史版本,降低響應時間。

MVVC是通過undo log和Read View兩種技術實現的。

二、快照讀與當前讀

MVCC在MySQL InnoDB中的實現主要是為瞭提高數據庫並發性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞並發讀 ,而這個讀指的就是快照讀 , 而非當前讀。當前讀實際上是一種加鎖的操作。

1.當前讀

當前讀讀取的記錄一定是最新的數據,讀取時還要保證其他並發事務不能修改當前記錄,會對讀取的記錄進行加鎖。

加鎖的讀被稱為當前讀,還有數據的增刪改都是要先讀取數據的,這一讀取過程也是當前讀。

SELECT * FROM t LOCK IN SHARE MODE; # 共享鎖
SELECT * FROM t FOR UPDATE; # 排他鎖
UPDATE SET t..

2.快照讀

快照讀又叫一致性讀,讀取的是數據行的快照版本。在MySQL中,普通的select語句(不加for update或lock in share mode的select語句)默認就是使用的快照讀,不加鎖。

SELECT * FROM table WHERE ...

之所以這樣,是因為快照讀可以避免加鎖操作,降低開銷。

當事務的隔離級別是串行時,快照讀就沒有用瞭,會退化為當前讀。

三、隔離級別與版本鏈復習

隔離級別:

在MySQL中默認的隔離級別就是可重復讀RR,可以解決不可重復讀問題,在MySQL中,特別的還額外支持解決幻讀問題。

它是如何解決幻讀問題的呢?有兩種方式:

  • 使用間隙鎖和臨鍵鎖解決,簡而言之就是加鎖,在此期間其他事務不能夠插入數據
  • MVCC方式,無需加鎖,消耗低(缺點是沒有完全解決幻讀問題)。

undo log版本鏈:

對應InnoDB來說,聚簇索引中的每個記錄都包含瞭兩個必要的隱藏字段:

  • trx_id:每次一個事務對某條聚簇索引記錄進行改動時,都會把該事務的事務id賦值給trx_id隱藏列。
  • roll_pointer:回滾指針,每次修改數據時,都會把舊數據放入undo log日志中,新的數據指向該舊數據,做成一個版本鏈,該指針字段就稱為回滾指針,通過該指針可以找到修改前的數據。

舉例:

有一個id為8的事務創建瞭一條數據,那麼該記錄的示意圖大概如下:

假設之後兩個id分別為10、20的事務對這條記錄進行update操作,流程如下:

事務10 事務20
BEGIN;  
  BEGIN;
UPDATE student SET name='李四' WHERE id=1;  
UPDATE student SET name='王五' WHERE id=1;  
COMMIT;  
  UPDATE student SET name='趙六' WHERE id=1;
  UPDATE student SET name='錢七' WHERE id=1;
  COMMIT;

每次修改都會生成一個undo log日志,每個日志都相互鏈接,構成版本鏈,此時該條數據的示意圖如下:

每個版本中還包含生成該版本時對應的事務id 。

四、Read View

有瞭undo log就可以讀取到記錄的歷史版本,那麼在什麼情況下,讀取哪個版本的記錄呢?這就用到瞭Read View,它幫我們解決瞭行的可見性問題。

Read View就是當某個事務在使用MVVC機制進行快照讀操作時產生的讀視圖。該視圖是數據庫當前所有活躍事務id(還未提交的事務)組成的列表的一個快照。

1.實現原理

四種隔離級別裡,讀未提交和串行化是不會使用MVVC的,因為讀未提交直接讀取某個數據的最新數據即可,串行化是通過加鎖來讀的。

讀已提交和可重復讀都必須保證讀到的數據都是其他事務提交瞭的,所以,其他事務修改瞭數據但是還未提交,我們不能夠訪問該數據,但可以通過MVVC機制讀取該記錄的歷史版本,核心問題就是需要判斷版本鏈中的哪條歷史版本是當前事務可見的,這也是ReadView要解決的問題。

Read View包含4個比較重要的內容:

  • creator_trx_id:創建這個Read View的事務id,Read View和事務是一一對應的。

隻有事務對表中的記錄做修改時才會為事務分配事務id,否則一個事務中隻有讀操作,該事務的id默認為0。

  • trx_ids:表示在生成Read View時當前系統中活躍的事務id列表。提交瞭的事務不在其中。
  • up_limit_id:活躍的事務中最小的事務id。
  • low_limit_id:表示生成Read View時系統應該分配給下一個事務的id值,同樣也表示系統中最大的事務id值。

註意:low_limit_id並不是trx_ids中的最大值,事務id是遞增分配的。比如,現在有id為1, 2,5這三個事務,之後id為5的事務提交瞭。那麼一個新的讀事務在生成ReadView時, trx_ids就包括1和2,up_limit_id的值就是1,low_limit_id的值就是6。

2.Read View規則

版本鏈

當某個事務有瞭Read View,訪問某條記錄時,需要按照下面的步驟判斷該記錄的哪個版本可見:

  • 如果該版本記錄的trx_id和Read View的creator_trx_id相同,意味著該版本的記錄是由當前事務修改的,因此該版本可以被當前事務訪問
  • 如果該版本記錄的trx_id小於Read View的up_limit_id,證明當前事務生成Read View時,此事務已經提交瞭,所以當前事務可以讀取該版本。
  • 如果該版本的trx_id大於等於low_limit_id,證明生成該版本的事務在當前事務生成Read View之後才開啟,所以該版本不可以被當前事務訪問。
  • 如果被訪問版本的trx_id屬性值在ReadView的up_limit_id和low_limit_id之間,那就需要判斷一下trx_id屬性值是不是在trx_ids列表中,如果不在的話才能訪問,否則不能訪問。

3.整體流程

瞭解瞭這些概念之後,我們來看下當查詢一條記錄的時候,系統如何通過MVCC找到它:

  • 首先獲取事務自己的版本號,也就是事務ID;
  • 獲取 ReadView;
  • 查詢得到的數據,然後與 ReadView 中的事務版本號進行比較;
  • 如果不符合 ReadView 規則,就需要從Undo Log中獲取歷史快照;
  • 最後返回符合規則的數據。

在隔離級別為讀已提交時,一個事務中的每一次SELECT查詢都會重新獲取一次Read View,而可重復讀是第一SELECT操作才會生成Read View,之後的查詢操作復用這一個。

導致這兩種的差距是因為:可重復讀要保證一個事務中相同的SELECT讀取的內容是相同的。

五、舉例

1.READ

COMMITTED隔離級別下

現在有兩個事務id分別為10、20的事務在執行:

-- id為10的事務
begin;
update t set name='李四' where id=1;
update t set name='王五' where id=1;
-- id為20的事務
更新其他行的數據

此刻,表中id為1的記錄得到的版本鏈表如下所示:

此時新來一個事務執行如下操作:

begin;
select * from t where id=1;
-- 事務10、20未提交

查詢到的結果為張三。

具體的過程如下:

  • 在執行select語句前,先生成一個Read View,Read View的creator_trx_id為0,trx_ids列表的內容是[10,20],up_limit_id為10,low_limit_id為21。
  • 查詢name為王五的最新版本的記錄,按規則進行對比,因為trx_id為10,10剛好是trx_ids中的記錄,所以這條記錄對當前事務不可見,根據回滾指針得到下一個版本
  • 下一個版本name為李四,也不行
  • 繼續找到name為張三的版本,trx_id為8,8小於up_limit_id,所以該版本對當前事務可見,得到最終結果

接下來,再將id為10的事務進行commit提交。然後id為20的事務來更新記錄:

begin;
-- id為20的事務
update t set name='趙六' where id=1;
update t set name='錢七' where id=1;

此時版本鏈更新為:

再到剛才使用READ COMMITTED隔離級別的事務中繼續查找這個id 為1的記錄,得到的結果為name=王五的那條記錄。執行過程如下:

  • 生成Read View,Read View的creator_trx_id為0,trx_ids列表的內容是[20],up_limit_id為20,low_limit_id為21。
  • 因為前兩個版本的記錄trx_id為20,存在trx_ids中,所以跳過
  • 到第三條記錄時,trx_id為10,小於20,可以讀取,所以最終結果為王五

註意:READ COMMITTED,每次讀取數據前都生成一個新的ReadView。

2.REPEATABLE READ隔離級別下

假如此時id為10的事務和id為20的事務正在修改,都未提交,修改內容和前面的一樣,但是還未提交,此時當前事務做一個查詢。

步驟為:

  • 生成Read View,Read View的creator_trx_id為0,trx_ids列表的內容是[10,20],up_limit_id為10,low_limit_id為21。
  • trx_id為10和20的都不滿足要求
  • 最後查找到name為張三的歷史版本的數據

此時,id為10的記錄提交事務。

當前事務又需要select id為1的記錄,步驟為:

  • 因為是可重復讀,且第一次select已經生成過Read View瞭,所有會復用它,不重新生成。
  • 所以trx_id為10和20的記錄依舊不符合規則,最終得到的數據還是張三,符合可重復讀的規范

註意:REPEATABLE READ,每次讀取都復用第一次生成的Read View

3.如何解決幻讀

假設現在有一條數據,id為1

當前活躍的事務有10和20。

此時當前事務啟動瞭,執行如下SQL語句:

begin;
select * from student where id>=1;

在開始前生成Read View,內容如下:creator_trx_id=0,trx_ids= [10,20] , up_limit_id=10, low_limit_id=21。

由於id大於等於1的數據隻有一個,且該數據的trx_id為8,小於up_limit_id,所以可以讀取到。

在這之後id為10的事務新增瞭一行數據,增加瞭id為2的數據,且提交瞭。

此時當前線程繼續查找id>=1的數據,因為是可重復讀,復用剛剛的Read View。

得到兩行數據,但是因為id為2的數據trx_id為10,該值在Read View的trx_ids中存在,所以該記錄對當前事務不可見,所以最後查詢到的數據隻有一條記錄。

如果當前事務再插入id為2的數據就插不進去,所以說MVVC隻解決瞭一半的幻讀問題。

到此這篇關於MySQL MVVC多版本並發控制的實現詳解的文章就介紹到這瞭,更多相關MySQL MVVC內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: