天天看點

Mysql 事務隔離級别和MVCC的關系

作者:做好一個程式猿

1 隔離性與隔離級别

提到事務,肯定會想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一緻性、隔離性、持久性),今天我們就來說說其中 I,也就是“隔離性”。

當資料庫上有多個事務同時執行的時候,就可能出現髒讀(dirty read)、不可重複讀(non-repeatable read)、幻讀(phantomread)的問題,為了解決這些問題,就有了“隔離級别”的概

念。

在談隔離級别之前,首先要知道,你隔離得越嚴實,效率就會越低。是以很多時候,我們要在二者之間尋找一個平衡點。SQL的事務隔離級别包括:

  • 讀未送出(read uncommitted)
    • 讀未送出是指,一個事務還沒送出時,它做的變更就能被别的事務看到。
  • 讀送出(read committed)
    • 讀送出是指,一個事務送出之後,它做的變更才會被其他事務看到。
  • 可重複讀(repeatable read)
    • 可重複讀是指,一個事務執行過程中看到的資料,總是跟這個事務在啟動時看到的資料是一緻的。當然在可重複讀隔離級别下,未送出變更對其他事務也是不可見的。
  • 串行化(serializable )
  • 串行化,顧名思義是對于同一行記錄,“寫”會加“寫鎖”,“讀”會加“讀鎖”。當出現讀寫鎖沖突

的時候,後通路的事務必須等前一個事務執行完成,才能繼續執行。

1.1 “讀送出”和“可重複讀”

假設資料表T中 隻有一列,其中一行的值為1,下面是按照時間順序執行兩個事務的行為。

mysql> create table T(c int) engine=InnoDB; 
insert into T(c) values(1);           
Mysql 事務隔離級别和MVCC的關系

我們來看看在不同的隔離級别下,事務A會有哪些不同的傳回結果,也就是圖裡面V1、V2、V3

的傳回值分别是什麼。

  1. 若隔離級别是“讀未送出”, 則V1的值就是2。這時候事務B雖然還沒有送出,但是結果已經被A看到了。是以,V2、V3也都是2。
  2. 若隔離級别是“讀送出”,則V1是1,V2的值是2。事務B的更新在送出後才能被A看到。是以,V3的值也是2。
  3. 若隔離級别是“可重複讀”,則V1、V2是1,V3是2。之是以V2還是1,遵循的就是這個要求:事務在執行期間看到的資料前後必須是一緻的。
  4. 若隔離級别是“串行化”,則在事務B執行“将1改成2”的時候,會被鎖住。直到事務A送出後,事務B才可以繼續執行。是以從A的角度看, V1、V2值是1,V3的值是2。

在實作上,資料庫裡面會建立一個視圖,通路的時候以視圖的邏輯結果為準。在“可重複讀”隔離級别下,這個視圖是在事務啟動時建立的,整個事務存在期間都用這個視圖。在“讀送出”隔離級别下,這個視圖是在每個SQL語句開始執行的時候建立的。這裡需要注意的是,“讀未送出”隔離級别下直接傳回記錄上的最新值,沒有視圖概念;而“串行化”隔離級别下直接用加鎖的方式來避免并行通路。

2 事務隔離的實作

了解了事務的隔離級别,我們再來看看事務隔離具體是怎麼實作的。這裡我們展開說明“可重複

讀”。

在MySQL中,實際上每條記錄在更新的時候都會同時記錄一條復原操作。記錄上的最新值,通過復原操作,都可以得到前一個狀态的值。

假設一個值從1被按順序改成了2、3、4,在復原日志裡面就會有類似下面的記錄。

Mysql 事務隔離級别和MVCC的關系

目前值是4,但是在查詢這條記錄的時候,不同時刻啟動的事務會有不同的read-view。如圖中看到的,在視圖A、B、C裡面,這一個記錄的值分别是1、2、4,同一條記錄在系統中可以存在多個版本,就是資料庫的多版本并發控制(MVCC)。

對于read-viewA,要得到1,就必須将目前值依次執行圖中所有的復原操作得到。同時你會發現,即使現在有另外一個事務正在将4改成5,這個事務跟read-viewA、B、C對應的事務是不會沖突的。

復原日志總不能一直保留吧,什麼時候删除呢?答案是,在不需要的時候才删除。也就是說,系統會判斷,當沒有事務再需要用到這些復原日志時,復原日志會被删除。什麼時候才不需要了呢?就是當系統裡沒有比這個復原日志更早地read-view的時候。

3 事務到底隔離還是不隔離

舉一個例子,下面是一個隻有兩行的表的初始化語句。

mysql> CREATE TABLE `t` ( 
  `id` int(11) NOT NULL, 
  `k` int(11) DEFAULT NULL, 
  PRIMARY KEY (`id`) ) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);           
Mysql 事務隔離級别和MVCC的關系

begin/start transaction 指令并不是一個事務的起點,在執行到它們之後的第一個操作InnoDB表 的語句,事務才真正啟動。如果你想要馬上啟動一個事務,可以使用start transaction with consistent snapshot 這個指令。

在這個例子中,事務C沒有顯式地使用begin/commit,表示這個update語句本身就是一個事務,語句完成的時候會自動送出。事務B在更新了行之後查詢; 事務A在一個隻讀事務中查詢,并且時間順序上是在事務B的查詢之後。

這時,如果我告訴你事務B查到的k的值是3,而事務A查到的k的值是1,你是不是感覺有點暈呢?今天這篇文章,我其實就是想和你說明白這個問題,希望借由把這個疑惑解開的過程,能夠幫助你對InnoDB的事務和鎖有更進一步的了解。

在MySQL裡,有兩個“視圖”的概念:

  • 一個是view。它是一個用查詢語句定義的虛拟表,在調用的時候執行查詢語句并生成結果。建立視圖的文法是create view…,而它的查詢方法與表一樣。
  • 另一個是InnoDB在實作MVCC時用到的一緻性讀視圖,即consistent read view,用于支援RC(Read Committed,讀送出)和RR(Repeatable Read,可重複讀)隔離級别的實作。 它沒有實體結構,作用是事務執行期間用來定義“我能看到什麼資料”。

4 “快照”在MVCC裡是怎麼工作的?

在可重複讀隔離級别下,事務在啟動的時候就“拍了個快照”。注意,這個快照是基于整庫的。 這時,你會說這看上去不太現實啊。如果一個庫有100G,那麼我啟動一個事務,MySQL就要拷貝100G的資料出來,這個過程得多慢啊。可是,我平時的事務執行起來很快啊。

實際上,我們并不需要拷貝出這100G的資料。我們先來看看這個快照是怎麼實作的:

InnoDB裡面每個事務有一個唯一的事務ID,叫作transaction id。它是在事務開始的時候向InnoDB的事務系統申請的,是按申請順序嚴格遞增的。而每行資料也都是有多個版本的。每次事務更新資料的時候,都會生成一個新的資料版本,并且 把transaction id指派給這個資料版本的事務ID,記為rowtrx_id。同時,舊的資料版本要保留,并且在新的資料版本中,能夠有資訊可以直接拿到它。也就是說,資料表中的一行記錄,其實可能有多個版本(row),每個版本有自己的rowtrx_id。

如下圖所示,就是一個記錄被多個事務連續更新後的狀态。

Mysql 事務隔離級别和MVCC的關系

圖中虛線框裡是同一行資料的4個版本,目前最新版本是V4,k的值是22,它是被transaction id

為25的事務更新的,是以它的rowtrx_id也是25。

語句更新會生成undo log(復原日志),上圖中的三個虛線箭頭,就是undo log;而V1、V2、V3并不是實體上真實存在的,而是每次需要的時候根據目前版本和undo log計算出來的。比如,需要V2的時候,就是通過V4依 次執行U3、U2算出來。

明白了多版本和rowtrx_id的概念後,我們再來想一下,InnoDB是怎麼定義那個“100G”的快照的。

按照可重複讀的定義,一個事務啟動的時候,能夠看到所有已經送出的事務結果。但是之後,這

個事務執行期間,其他事務的更新對它不可見。是以,一個事務隻需要在啟動的時候聲明說,“以我啟動的時刻為準,如果一個資料版本是在我啟動之前生成的,就認;如果是我啟動以後才生成的,我就不認,我必須要找到它的上一個版本”。當然,如果“上一個版本”也不可見,那就得繼續往前找。還有,如果是這個事務自己更新的資料,它自己還是要認的。

在實作上, InnoDB為每個事務構造了一個數組,用來儲存這個事務啟動瞬間,目前正在“活躍”的所有事務ID。“活躍”指的就是,啟動了但還沒送出。 數組裡面事務ID的最小值記為低水位,目前系統裡面已經建立過的事務ID的最大值加1記為高水位。這個視圖數組和高水位,就組成了目前事務的一緻性視圖(read-view)。 而資料版本的可見性規則,就是基于資料的rowtrx_id和這個一緻性視圖的對比結果得到的。 這個視圖數組把所有的rowtrx_id 分成了幾種不同的情況。

Mysql 事務隔離級别和MVCC的關系

這樣,對于目前事務的啟動瞬間來說,一個資料版本的rowtrx_id,有以下幾種可能:

  1. 如果落在綠色部分,表示這個版本是已送出的事務或者是目前事務自己生成的,這個資料是可見的;
  2. 如果落在紅色部分,表示這個版本是由将來啟動的事務生成的,是肯定不可見的;
  3. 如果落在黃色部分,那就包括兩種情況
    1. 若 rowtrx_id在數組中,表示這個版本是由還沒送出的事務生成的,不可見;
    2. 若 rowtrx_id不在數組中,表示這個版本是已經送出了的事務生成的,可見。

我們再看這個圖中的三個事務,分析下事務A的語句傳回的結果,為什麼是k=1。

Mysql 事務隔離級别和MVCC的關系

這裡,我們不妨做如下假設:

  1. 事務A開始前,系統裡面隻有一個活躍事務ID是99;
  2. 事務A、B、C的版本号分别是100、101、102,且目前系統裡隻有這四個事務;
  3. 三個事務開始前,(1,1)這一行資料的rowtrx_id是90。

這樣,事務A的視圖數組就是[99,100], 事務B的視圖數組是[99,100,101], 事務C的視圖數組是

[99,100,101,102]。

第一個有效更新是事務C,把資料從(1,1)改成了(1,2)。這時候,這個資料的最新版本的rowtrx_id是102,而90這個版本已經成為了曆史版本。

第二個有效更新是事務B,把資料從(1,2)改成了(1,3)。這時候,這個資料的最新版本(即row

trx_id)是101,而102又成為了曆史版本。

你可能注意到了,在事務A查詢的時候,其實事務B還沒有送出,但是它生成的(1,3)這個版本已

經變成目前版本了。但這個版本對事務A必須是不可見的,否則就變成髒讀了。

對于一個事務視圖來說,除了自己的更新總是可見以外,有三種情況:

  1. 版本未送出,不可見;
  2. 版本已送出,但是是在視圖建立後送出的,不可見;
  3. 版本已送出,而且是在視圖建立前送出的,可見。

疑問:事務B的update語句,如果按照一緻性讀,好像結果不對?

事務B的視圖數組是先生成的,之後事務C才送出,不是應該看不見(1,2)嗎,怎麼算出(1,3)來?

如果事務B在更新之前查詢一次資料,這個查詢傳回的k的值确實是1。但是,當它要去更新資料的時候,就不能再在曆史版本上更新了,否則事務C的更新就丢失了。是以,事務B此時的set k=k+1是在(1,2)的基礎上進行的操作。

是以,這裡就用到了這樣一條規則:更新資料都是先讀後寫的,而這個讀,隻能讀目前的值,稱為“目前讀”。

是以,在更新的時候,目前讀拿到的資料是(1,2),更新後生成了新版本的資料(1,3),這個新版本的rowtrx_id是101。是以,在執行事務B查詢語句的時候,一看自己的版本号是101,最新資料的版本号也是101,是自己的更新,可以直接使用,是以查詢得到的k的值是3。

這裡我們提到了一個概念,叫作目前讀。其實,除了update語句外,select語句如果加鎖,也是

目前讀。是以,如果把事務A的查詢語句select *fromt where id=1修改一下,加上lock in share mode 或 for update,也都可以讀到版本号是101的資料,傳回的k的值是3。下面這兩個select語句,就是分别加了讀鎖(S鎖,共享鎖)和寫鎖(X鎖,排他鎖)。

mysql> select k from t where id=1 lock in share mode; 
mysql> select k from t where id=1 for update;            

5 總結

5.1 小節

MVCC,多版本的并發控制,Multi-Version Concurrency Control。

使用版本來控制并發情況下的資料問題,在B事務開始修改賬戶且事務未送出時,當A事務需要讀取賬戶餘額時,此時會讀取到B事務修改操作之前的賬戶餘額的副本資料,但是如果A事務需要修改賬戶餘額資料就必須要等待B事務送出事務。

MVCC使得資料庫讀不會對資料加鎖,普通的SELECT請求不會加鎖,提高了資料庫的并發處理能力。借助MVCC,資料庫可以實作READ COMMITTED,REPEATABLE READ等隔離級别,使用者可以檢視目前資料的前一個或者前幾個曆史版本,保證了ACID中的I特性(隔離性)。

5.2 InnoDB的MVCC實作邏輯

MVCC可以認為是行級鎖的一個變種,它可以在很多情況下避免加鎖操作,是以開銷更低。MVCC的實作大都都實作了非阻塞的讀操作,寫操作也隻鎖定必要的行。InnoDB的MVCC實作,是通過儲存資料在某個時間點的快照來實作的。一個事務,不管其執行多長時間,其内部看到的資料是一緻的。也就是事務在執行的過程中不會互相影響。下面我們簡述一下MVCC在InnoDB中的實作。

  InnoDB的MVCC,通過在每行記錄後面儲存兩個隐藏的列來實作:一個儲存了行的建立時間,一個儲存行的過期時間(删除時間),當然,這裡的時間并不是時間戳,而是系統版本号,每開始一個新的事務,系統版本号就會遞增。在RR隔離級别下,MVCC的操作如下:

  1. select操作。
    1. InnoDB隻查找版本早于(包含等于)目前事務版本的資料行。可以確定事務讀取的行,要麼是事務開始前就已存在,或者事務自身插入或修改的記錄。
    2. 行的删除版本要麼未定義,要麼大于目前事務版本号。可以確定事務讀取的行,在事務開始之前未删除。
  1. insert操作。将新插入的行儲存目前版本号為行版本号。
  2. delete操作。将删除的行儲存目前版本号為删除辨別。
  3. update操作。變為insert和delete操作的組合,insert的行儲存目前版本号為行版本号,delete則儲存目前版本号到原來的行作為删除辨別。

附:MVCC 在mysql 中的實作依賴的是 undo log 與 read view 。

繼續閱讀