MySQL是如何實現事務隔離?

前言

  衆所周知,MySQL的在RR隔離級別下查詢數據,是能夠保證數據不受其它事物影響,而在RC隔離級別下只要其它事物commit後,數據都會讀到commit以後的數據,那麼事物隔離的原理是什麼?是經過什麼實現的呢?那確定是經過MVCC機制(Multi-Version Concurrency Control,即多版本併發控制),這是不少人知道的,可是我以前沒有好好分析過其實現原理,因此寫下此篇博文記錄下!數組

  注:MySQL的InnoDB引擎之因此可以支持高性能的併發性能,就是因爲MySQL的MVCC機制(歸功於undo log、Read-View、),可是本篇不對MVCC過多的介紹。併發

  參考資料:《MySQL實戰45講》系列,雖然講解的比較清晰,可是仍然須要理解,好比關於視圖數組部分我認爲是相比較而言沒有解釋清楚,因此結合資料與本身看法加以記錄!性能

 


 

1、RC與RR隔離級別

咱們分別開啓RC與RR隔離級別實驗說明,首先假設有account帳戶表,在事務ABC開啓前,帳戶中的餘額balance爲1,即spa

select balance from account =1; # 結果爲1

1.RR事務隔離級別下查詢結果

當在RR事務隔離級別分別開啓三個事務,在不一樣時間段內作以下操做3d

  • 事務A(顯式開啓事務,手動commit提交):查詢餘額
  • 事務B(顯式開啓事務,手動commit提交):對id=1的餘額加1
  • 事務C(不顯式開啓事務,自動提交):對id=1的餘額加1

 咱們從時間邏輯上分爲三個階段,分析結果版本控制

  • 第一階段:事務A立馬開始事務,隨後事務B也緊跟着立馬開始事務,而後事務C首先更新balance爲2成功,當前balance=2;
  • 第二階段:事務B更新balance的值,此時先讀到當前balance最新值爲2,隨後set balance=balance+1成功,當前balance=3;
  • 第三階段:事務A查詢balance的值,此時的值爲1(這裏爲何等於1呢,是怎麼實現的呢?不該該是當前最新值3嗎?這就是本篇博文討論的重點,最後commit結束事務,緊接着事務B也commit結束事務

 最後事務A讀取balance的結果是1,理所固然,RR即爲可重複讀,即一個事務在執行過程當中看到的數據,老是跟這個事務啓動時看到的數據是一致的,當前事務無論有沒有提交,都不會影響數據,我只須要讀取基於快照的數據便可,這就是快照讀。可是咱們要討論的是如何在MVCC機制下實現?code

注:begin/start transaction 命令並非一個事務的起點,在執行到它們以後的第一個操做InnoDB表的語句,事務才真正啓動。若是你想要立刻啓動一個事務,可使用start transaction with consistent snapshot 這個命令。orm

1.RC事務隔離級別下查詢結果

一樣地,咱們在RC隔離下,開啓事務ABC,觀察事務A最後的balance結果。blog

 最後事務A讀取balance的結果是2,理所固然,RC即爲讀可提交,字面意思就是其餘事務只要提交後,當前事務我就能立馬讀取到最新當前值,這就是當前讀。可是咱們要討論的是如何在MVCC機制下實現?事務

 實際上這是由於實現MVCC時用到的一致性讀視圖,即consistent read view,用於支持RC(Read Committed,讀提交)和RR(Repeatable Read,可重複讀)隔離級別的實現。

3、事務隔離在MVCC的實現

在探討MVCC如何實現事務隔離前,咱們須要知道是視圖數組、一致性視圖等概念,才能幫助更好理解MVCC幫助事務實現了隔離。

1.數據行ROW的多版本

  InnoDB裏面每一個事務有一個惟一的事務ID,叫做transaction id。它是在事務開始的時候向InnoDB的事務系統申請的,是按申請順序嚴格遞增的

  而每行數據也都是有多個版本的。每次事務更新數據的時候,都會生成一個新的數據版本,而且把transaction id賦值給這個數據版本的事務ID,記爲row trx_id。同時,舊的數據版本要保留,而且在新的數據版本中,可以有信息能夠直接拿到它(經過undo_log文件找到)。

  也就是說,數據表中的一行記錄,其實可能有多個版本(row),每一個版本有本身的row trx_id

  對某一個數據行ROW某個時刻通過三次更新事務的多版本控制流程,畫以下圖加深理解。

從圖咱們能夠獲得:

  • ROW有四個版本V1-V4,即通過三次更新balance後,當前最新版本爲V4,當前balance已經更新爲4,是最新值
  • InnoDB每次更新事務產生的transaction id都會賦值給row trx_id;
  • 經過undo_log能夠從V4撤回到V1,找到V1版本的balance=1,即undo_log回滾版本。

明白了數據行的ROW的多版本原理與實現後,能夠幫助咱們理解InnoDB是怎麼定義並建立快照的!

2.視圖數組

  下述部分出自資料中的原句,特別是紅色加深部分可能會比較難以理解,因此須要結合本身理解並畫圖

InnoDB是這麼在事務開啓的時候定義快照的,哪些事務的操做我能夠忽視,哪麼我必需要保存在快照裏。能夠理解爲:一個事務只須要在啓動的時候聲明說,「以我啓動的時刻爲準,若是一個數據版本是在我啓動以前生成的,就認;若是是我啓動之後才生成的,我就不認,我必需要找到它的上一個版本」。

  在實現上, InnoDB爲每一個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正在「活躍」的全部事務ID。「活躍」指的就是,啓動了但還沒提交。數組裏面事務ID的最小值記爲低水位,當前系統裏面已經建立過的事務ID的最大值加1記爲高水位。這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。

我對低水位與高水位的理解:

低水位=當前全部啓動了但未提交事務集合的ID最小值=當前事務的上一個啓動但未提交的事務ID最小值(全部活躍事務ID最小值)

高水位=當前事務的ID(當前ROW版本號/row trx_id)=已經建立過事務ID的最大值+1

舉例說明:仍然以上述RR隔離級別下三個ABC事務爲例

  • 事務A開始前,系統裏面只有一個活躍事務ID是99;
  • 事務A、B、C的版本號分別是100、10一、102,且當前系統裏只有這四個事務;
  • 三個事務開始前,(id,balance)=(1,1)這一行數據的row trx_id是90。

這樣,事務A的視圖數組就是[99], 事務B的視圖數組是[99,100], 事務C的視圖數組是[99,100,101]即視圖數組通用公式爲:[{當前事務開啓瞬間活躍事務ID集合}]

 數據版本的可見性規則,就是基於row trx_id和一致性視圖對比結果獲得的,因此咱們還必須再瞭解下一致性視圖

3.一致性視圖

經過對視圖數組的理解,一致性視圖就更加容易了,即:這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。

仍然以上述RR隔離級別下三個ABC事務爲例

  • 事務A開始前,系統裏面只有一個活躍事務ID是99, 因此事物A開啓瞬間活躍事物集合爲[99];
  • 事務A、B、C的版本號分別是100、10一、102,且當前系統裏只有這四個事務,因此事物A、B、C高水位分別爲100、10一、102
  • 三個事務開始前,(id,balance)=(1,1)這一行數據的row trx_id是90。

 這樣,事務A的一致性視圖就是[99,100], 事務B的一致性視圖是[99,100,101], 事務C的一致性視圖是[99,100,101,102]。即一致性視圖通用公式爲:[{當前事務開啓瞬間活躍事務ID集合},當前row trx_id]

分析上述流程圖結果:

第一個有效更新版本是事物C,更新balance=2,這個時候的最新版本row trx_id=102,而以前的在事物ABC以前的活躍事物最新版本row trx_id爲99,因此此時99已經成爲歷史版本1;

第二個有效更新版本是事物B,更新balance=3,這個時候最新版本row trx_id=101,而此時row trx_id=102成爲歷史版本1,而row trx_id=99成爲歷史版本2;

事物A查詢的時候,事物B是沒有提交,但生成的(id, balance)=(1, 3)已經成爲當前最新版本,事物A讀取數據時,一致性視圖爲[99, 100],而讀數據都是從當前版本切的而後對比row trx_id,因此會有如下流程:

  • 找到(1,3)的時候,判斷出row trx_id=101,比高水位大,處於紅色區域,不可見;
  • 接着,找到上一個歷史版本,一看row trx_id=102,比高水位大,處於紅色區域,不可見;
  • 再往前找,終於找到了(1,1),它的row trx_id=90,比低水位小,處於綠色區域,可見

最後事物A不管在何時查詢,看到的數據都是一致性視圖[99, 100]生成的快照數據(1, 1),即row trx_id=90時的數據。這就稱之爲一致性讀

總結:

對於一個事務視圖來講,除了本身的更新老是可見之外,有三種狀況:
  • 版本未提交,不可見;
  • 版本已提交,可是是在視圖建立後提交的,不可見;
  • 版本已提交,並且是在視圖建立前提交的,可見。
如今,咱們用這個規則來判斷圖中的查詢結果,事務A的查詢語句的視圖數組是在事務A啓動的
時候生成的,這時候:
  • (1,3)還沒提交,屬於狀況1,不可見;
  • (1,2)雖然提交了,可是是在視圖數組建立以後提交的,屬於狀況2,不可見;
  • (1,1)是在視圖數組建立以前提交的,可見

4.當前讀與快照讀

(1)當前讀與快照讀規則

  固然按照這個一致性讀的邏輯,事物B在事物C有效更新balance=2以後,可是事物B的視圖數組是在事物C生成的,因此理論上來講不該該是事物B看到的是(id, balance)=(1, 1)這個數據(快照/歷史版本)嗎?而看不到當前版本(1, 2)數據。爲何事物B在更新balance以後直接數據就成爲(1, 3)了呢?

  若是事物B在update以前select一次數據,看到的值確實是balance=1,可是update是不能在歷史版本上操做的,不然事物C的更新就會丟失,因此update操做都是在先讀取當前版本,而後再更新。

  也就說有這麼一條規則:更新數據都是先讀後更新,而這個讀是讀當前最新值,稱之爲「當前讀(current read),而只查詢不讀的話就會讀取當前快照,稱之爲「快照讀」因此在事物B更新balance以前,先查詢到最新的版本(1, 2)而後再更新爲(1, 3)。而事物A查詢的快照數據爲(1, 1),而不是最新版本(1, 3)。

(2)當前讀與快照讀解釋

  當前讀:像select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操做都是一種當前讀。就是它讀取的是記錄的最新版本,讀取時還要保證其餘併發事務不能修改當前記錄,會對讀取的記錄進行加鎖。

  快照讀:像不加鎖的select操做就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是串行級別,串行級別下的快照讀會退化成當前讀。是基於多版本控制的,那麼快照讀可能讀到的並不必定是數據的最新版本,而有多是以前的歷史版本(快照數據)

(3)RC讀可提交下的視圖規則

讀提交的邏輯和可重複讀的邏輯相似,它們最主要的區別是:

  • 在可重複讀隔離級別下,只須要在事務開始的時候建立一致性視圖,以後事務裏的其餘查詢,都共用這個一致性視圖
  • 在讀提交隔離級別下,每個語句執行前都會從新算出一個新的視圖,此時start transaction with consistent snapshot就等同於普通的start transaction/begin
因此在RC隔離級別下,事物A與事物B查詢到的數據分別以下:
  • 事物C立馬更新balance=2,而後自動提交,生成最新版本(1, 2),此時從新計算出視圖數據(1, 2);
  • 事物B查到此時的最新版本爲(1, 2),以後再更新爲版本(1, 3)爲當前最新版本,查詢此時的事物B select到的balance=3(事物B更新balance=3以後立馬算出一個新的視圖,select就是根據此視圖獲得的數據),而不是1。
  • 而此時事物B還未提交,對於事物A來講是看不見的,因此事物A此時讀取到的事物C提交的最新版本(1, 2)

 

相關文章
相關標籤/搜索