MySQL系列(9)— 事務隔離性之MVCC

專欄系列文章:MySQL系列專欄web

MVCC

MVCC 介紹

前面在 事務原子性之UndoLog 這篇文章中屢次提到過MVCC這個東西了,咱們說執行DELETE語句或者更新主鍵UPDATE語句並不會當即把對應的記錄直接從頁面中刪除,而是將記錄頭的delete_mask設置爲1,作標記刪除,這主要就是爲MVCC服務的。數據庫

而後在 事務基礎 這篇文章中介紹了併發事務會有 髒寫、髒讀、不可重複讀、幻讀 四個問題,髒寫能夠經過樂觀鎖或悲觀鎖的方式來解決,髒讀、不可重複讀、幻讀 三個問題須要數據庫提供必定的事務隔離機制來解決,也就是事務的隔離性。markdown

SQL標準的事務隔離級別有四個,分別能解決併發事務的一些問題:併發

image.png

髒讀、不可重複讀、幻讀 說的都是併發讀取的問題,最簡單的方式就是給記錄加一把鎖,不論是更新、讀取記錄都須要競爭到這把鎖以後才能操做。但這種方式的併發性能可想而知會有多麼低。post

因而 InnoDB 就設計了MVCC來解決併發讀取的問題,MVCC 就是多版本併發控制(Multi-Version Concurrency Control)。在 RCRR 這兩種隔離級別下執行SELECT查詢時,經過訪問記錄的版本鏈,而不須要加鎖,這樣使得不一樣事務的讀-寫操做能夠併發執行,從而提高數據庫的性能。性能

undo log 版本鏈

前面說 MVCC 是讀取記錄的版本鏈實現的,這個版本鏈其實就是由undo log造成的一個版本鏈條。url

事務原子性之UndoLog 文章中的這幅圖爲例,很直觀的能夠看到,增刪改產生的 undo log 經過old roll_pointer連成一個單向鏈表,記錄中的隱藏列roll_pointer則指向最新的一個undo log,就是undo版本鏈的頭結點。spa

image.png

記錄中始終都是最新更新的值,可能更新這條記錄的事務還未提交:設計

  • 對於使用 RU 隔離級別的事務來講,因爲能夠讀到未提交事務修改過的記錄,因此直接讀取記錄的最新版本就行了。3d

  • 對於使用 SERIALIZABLE 隔離級別的事務來講,InnoDB 使用加鎖的方式來訪問記錄。這個後面再說。

  • 對於使用 RCRR 隔離級別的事務來講,都必須保證讀到已提交事務修改過的記錄,若是另外一個事務修改的記錄還未提交,是不能直接讀取記錄的最新版本的,此時就能夠沿着undo版本鏈查找當前事務可見的版本。

ReadView

上一節說到在RC、RR隔離級別下,要保證讀到已提交事務修改過的記錄,就要在undo版本鏈上找到當前事務可見的版本。那如何判斷版本鏈上的哪一個版本是當前事務可見的呢?

InnoDB 設計了一個 ReadView,在執行一個事務的時候就會建立一個ReadView

ReadView 有四個關鍵屬性:

  • m_ids:在生成 ReadView 時當前系統中活躍的事務的事務ID列表。
  • min_trx_id:生成 ReadView 時當前系統中活躍的事務中最小的事務ID,也就是m_ids中的最小值。
  • max_trx_id:生成 ReadView 時系統中分配給下一個事務的ID值,就是全局事務ID(Max Trx Id),注意並非m_ids中的最大值。
  • creator_trx_id:生成該 ReadView 的事務的事務ID。事務中只有在執行了增刪改操做時纔會分配一個事務ID,若是是一個只讀事務,那 creator_trx_id 默認就爲0

MVCC 原理

undo版本鏈+ReadView機制

有了ReadView後,在事務中查詢的時候,就能夠沿着 undo 版本鏈查找當前事務可見的版本。這時 undo log 中的隱藏列 trx_id 就派上用場了,它表示產生這條 undo log 時的事務的事務ID。判斷此版本是否可訪問的依據就是用 undo log 中的 trx_id 屬性值與 ReadView 中的各個屬性作比較。

經過以下步驟來判斷版本是否可被訪問:

  • ① 若是 trx_id 等於 creator_trx_id ,說明當前事務在訪問它本身修改過的記錄,因此該版本能夠被當前事務訪問。

  • ② 若是 trx_id 小於 min_trx_id,說明生成該版本的事務在當前事務生成 ReadView 前已經提交,因此該版本能夠被當前事務訪問。

  • ③ 若是 trx_id 大於或等於max_trx_id,說明生成該版本的事務在當前事務生成 ReadView 後纔開啓,因此該版本不能夠被當前事務訪問。

  • ④ 若是 trx_idmin_trx_idmax_trx_id 之間,此時再判斷一下 trx_id 是否是在 m_ids 列表中,若是在,說明建立 ReadView 時生成該版本的事務仍是活躍的,該版本不能夠被訪問;若是不在,說明建立 ReadView 時生成該版本的事務已經被提交,該版本能夠被訪問。

大體的流程圖以下:

image.png

RC 和 RR 隔離級別

READ COMMITTEDREPEATABLE READ 隔離級別的區別就是它們生成ReadView的時機不一樣。

  • READ COMMITTED 是每次查詢前都會生成一個獨立的 ReadView。
  • REPEATABLE READ 則只在第一次查詢前生成一個 ReadView,以後的查詢都重複使用這個 ReadView。
  • READ UNCOMMITTED 則不須要生成 ReadView,直接讀取行記錄的數據。

仍是以以前的 account 表爲例,下面按照操做發生的時間順序來進行說明。

一、T1

如今 account 表中的初始狀態以下,最後更新這條數據的事務ID爲100,card 的值爲 AA

系統下一個要分配的事務ID爲150,而後系統有兩個事務正在運行,事務ID分別爲130、135

image.png

二、T2

此時新開一個事務A,查詢ID=1的數據,就會生成一個 ReadView 以下圖所示:

image.png

此時會先判斷記錄中的 trx_id(100) 與 creator_trx_id(0) 是否相等,這個條件不知足;

接着判斷 trx_id(100) < min_trx_id(130),這個條件知足,那就直接讀取這行數據。

此時在事務A中查詢 balance=0 的數據,也只會返回1條數據。

三、T3

接着另外一個事務B(trx_id=150)更新這條數據,將 AA 更新爲 BB,事務還未提交。

image.png

接着在事務A中再次查詢ID=1的這條數據。

  • RC 隔離級別下,會生成一個新的 ReadView:

image.png

先判斷行記錄,發現 trx_id(150) 在 min_trx_id(130) 和 max_trx_id(160) 之間,同時 m_ids(130,135,150) 列表中,因此記錄行上的數據對本事務不可見;而後繼續對比以前的版本,發現 AA 這條版本的 trx_id(100) < min_trx_id(130),因此就返回 AA 這個版本。因此在 RC 隔離級別下屢次讀取,看不到別的事務未提交的更新,可避免髒讀的問題。

  • RR 隔離級別下,ReadView 不變:

image.png

先判斷行記錄,發現 trx_id(150) 等於 max_trx_id(150),因此記錄行上的數據對本事務不可見;而後繼續對比以前的版本,發現 AA 這條版本的 trx_id(100) < min_trx_id(130),因此就返回 AA 這個版本。因此在 RR 隔離級別下屢次讀取,看不到別的事務未提交的更新,可避免髒讀的問題。

四、T4

接着事務B提交了更新。

image.png

接着在事務A中再次查詢ID=1的這條數據。

  • RC 隔離級別下,會生成一個新的 ReadView:

image.png

先判斷行記錄,發現 trx_id(150) 在 min_trx_id(130) 和 max_trx_id(165) 之間,同時不在 m_ids(130,135) 列表中,因此記錄行上的數據對本事務可見,返回 BB 這個版本。因此在 RC 隔離級別下屢次讀取,是能夠看到別的事務已提交的更新,會有不可重複讀的問題。

  • RR 隔離級別下,ReadView 不變:

image.png

此時的判斷跟上一步中的判斷是同樣的,最後也是返回 AA 個版本。因此在 RR 隔離級別下屢次讀取,看不到別的事務已提交的更新,避免了不可重複讀的問題。

五、T5

接着一個新的事務(trx_id=175)更新ID=1這條數據,將 BB 更新爲 CC,同時還插入了ID=2這條數據,且事務已提交。

image.png

這時在事務A中查詢 balance=0 的數據。

  • RC 隔離級別下,會生成一個新的 ReadView:

image.png

查詢ID=1這行記錄時,先判斷行版本,因爲 trx_id(175) 在 min_trx_id(170) 和 max_trx_id(200) 之間,且不在 m_ids(170,180) 列表中,因此返回 CC 這個版本。查詢ID=2這行記錄時,一樣的判斷過程,會返回 MM 這個版本。最終查詢返回2條數據,而最開始查詢只返回1條數據,因此在 RC 隔離級別下屢次讀取,會有幻讀的問題。

  • RR 隔離級別下,ReadView 不變:

image.png

查詢ID=1這行記錄時,最終會沿着版本鏈找到 AA 這個版本。查詢ID=2這行記錄時,trx_id(175) > max_trx_id(150),因此ID=2這行記錄不匹配。最終查詢只返回1條數據,因此在 RR 隔離級別下屢次讀取,不會有幻讀的問題。

六、T6

接着一個新的事務(trx_id=205)刪除了ID=1這條數據,但刪除的時候並非真正的刪除,只是將delete_mask標記爲 1

image.png

接着在事務A中再次查詢ID=1的這條數據。

因爲行記錄中 delete_mask 標記爲 1 了,是不能被查詢的,因此只能沿着版本鏈查詢以前的版本。以後的匹配過程跟前面的描述是相似的,就不在贅述了。在 RC 隔離級別下,會返回 trx_id=175,值爲 CC 這個版本。在 RR 隔離級別下,會返回 trx_id=100,值爲 AA 這個版本。

MVCC 總結

從上面示例的演示過程就能夠看出,MVCC 就是經過 undo log 版本鏈 + ReadView 實現的一套併發讀取的機制。

READ COMMITTD 隔離級別下,每次查詢都生成一個新的 ReadView,不能讀到別的事務未提交的修改,所以解決了 髒讀 的問題。可是能讀取到別的事務已提交的修改,會有 不可重複讀、幻讀 的問題。

REPEATABLE READ 隔離級別下,只在第一次查詢時生成一個 ReadView,以後的查詢都重複使用這個 ReadView。別的事務未提交、已提交、新插入的修改都讀取不到,所以解決了 髒讀、不可重複讀、幻讀 的問題。

前面介紹 undo log 的文章說過,執行 DELETE 語句或者更新主鍵的 UPDATE 語句並不會當即把對應的記錄徹底從頁面中刪除,而是將 delete_mask 設置爲 1,作標記刪除。這時就清楚是爲何了,這主要就是爲MVCC服務的,由於可能有其它併發運行的事務,要經過版本鏈讀取當前事務可見的版本。

快照讀和當前讀

有一點須要注意的是,前面的示例中的查詢都是簡單的SELECT查詢,這種就是讀取undo版本鏈上的一個快照版本,能夠稱爲快照讀一致性非鎖定讀。因爲是讀取的快照,所以在RR隔離級別下能夠避免幻讀的發生。

但若是是INSERT、DELETE、UPDATE語句,例以下面的SQL,這個 UPDATE 語句會更新 balance=0 的記錄,這種方式就稱爲當前讀,讀取的是最新的數據。當前讀能讀取到別的事務已提交的修改,就可能會產生幻讀的問題。

UPDATE account SET balance=100 WHERE balance = 0;
複製代碼

例如,在默認RR隔離級別下,按以下順序執行SQL,Session B 兩次普通查詢的結果都是同樣的,沒有幻讀的問題。這是由於 Session B 第二次查詢讀取的是快照版本,即快照讀,不會讀取到別的事務提交的修改。

Timeline Session A Session B Session C
t1 TUNCATE TABLE account;
INSERT INTO account(card) VALUES ('AA');
t2 BEGIN; BEGIN;
t3 SELECT * FROM account WHERE balance=0;
(返回AA這條記錄)
t4 INSERT INTO account(card) VALUES ('BB');
t5 COMMIT;
t6 SELECT * FROM account WHERE balance=0;
(返回AA這條記錄)
u7 COMMIT;

再按照以下順序執行SQL,Session B 第一次查詢 balance=0 的數據只有AA這一條,而後更新 balance=0 的數據,按理來講只更新一條纔對,會發現更新了兩條數據,並且再次查詢返回了AA、BB這兩條數據,此時就產生了幻讀的問題。這是由於中間那次更新是當前讀,更新時的查詢能夠讀到其它事務提交的更新,此時MVCC是沒法解決這個問題的。

Timeline Session A Session B Session C
t1 TUNCATE TABLE account;
INSERT INTO account(card) VALUES ('AA');
t2 BEGIN; BEGIN;
t3 SELECT * FROM account WHERE balance=0;
(返回AA這條記錄)
t4 INSERT INTO account(card) VALUES('BB')
t5 COMMIT;
t6 UPDATE account SET balance=100 WHERE balance=0;
(會看到更新了兩行數據)
t7 SELECT * FROM account WHERE balance=100;
(返回AA、BB這兩條記錄)

那當前讀這種問題如何解決呢?這就要用到下篇文章中介紹的了。

相關文章
相關標籤/搜索