首頁 > 軟體

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

2022-08-24 18:05:03

一、概述

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內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com