mysql MVCC 介紹

簡介

MVCC (multiversion concurrency control),多版本併發控制,主要是經過在每一行記錄中增長三個字段,與 undo log 中相關記錄配合使用,同時加上可見性算法,使得各個事務能夠在不加鎖的狀況下可以同時地讀取到某行記錄上的準確值(這個值對不一樣的事務而言多是不一樣的)。使用 MVCC,在不加鎖的狀況下也能讀取到準確的數據,大大提升了併發效率。html

事務

提到 MVCC,必須提到事務。關於事務,有四個特性,即咱們常說的 ACID。mysql

  • 原子性(Atomicity):表示事務要麼所有執行,要麼所有不執行,這是一個不可分割的最小單元
  • 一致性(Consistency):表示事務老是從一個一致的狀態轉移到另外一個一致的狀態
  • 隔離性(Isolation):表示各個事務之間相關隔離,互不影響
  • 持久性(Durability):指一個事務一旦被提交,它對數據庫的改變就是永久性的,即便後續數據庫發生故障也不會有影響

而事務隔離性又分爲四種級別:讀未提交(read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)、串行化(serializable)。算法

  • 讀未提交:指一個事務尚未提交,它自己所作的修改就能被其餘事務所看到。在這種狀況下,會產生髒讀、幻讀和不可重複讀的問題。
  • 讀提交:指一個事務提交以後,它自己所作的修改就能被其餘事務所看到。在這種狀況下,解決了「讀未提交」的髒讀問題,可是仍然會產生幻讀和不可重複讀的問題。
  • 可重複讀:指在同一個事務之中,讀到的數據是一致的。這種隔離級別下,能夠解決髒讀和不可重複讀的問題,可是仍然存在幻讀的問題。
  • 串行化:指多個事務中,若是讀寫鎖衝突時,後訪問的事務必須等前一個事務執行完成後才能繼續執行。這種隔離級別最高,也解決了髒讀、幻讀和不可重複讀的問題。可是其也大大限制了併發的程度。

關於這四種隔離級別的差別,能夠經過如下例子(例子來源於:林曉斌:MySQL實戰45講)來加以說明。sql

假設存在一張表,裏面只有一個字段和一條記錄,值是 1,如今發生如下的操做數據庫

時刻 事務A
事務B
t1 啓動事務,查詢獲得值 1
t2
啓動事務
t3
查詢獲得值 1
t4
將 1 改爲 2
t5 查詢獲得值 V1

t6
提交事務
t7 查詢獲得值 V2

t8 提交事務

t9 查詢獲得值 V3

針對不一樣的隔離級別,V一、V二、V3 讀到的值不一樣。數組

在「讀未提交」的隔離級別下,因爲 t4 時刻事務 B 將值改爲了 2,雖然 B 還沒提交事務,可是此時的修改對其餘事務是可見的,因此 V一、V二、V3 查詢到的值都是 2。併發

在「讀提交」的隔離級別下,t4 時刻修改了值,可是在 t5 時刻,事務 B 尚未提交,此時事務 A 讀取到的值仍是老的值,因此 V1 是 1,而在 t7 時刻,因爲事務 B 已經在 t6 時刻提交了,此時事務 B 所作的修改對其餘的事務均可見,因此事務 A 在 t7 時刻能看到事務 B 的修改,此時 V2 的值爲 2,固然 V3 的值也爲 2。mvc

在「可重複讀」的隔離級別下,遵循 「事務在執行期間看到的數據必須是先後一致」 的要求,因此不管事務 B 是否修改值,也不管事務 B 是否提交,事務 A 在沒提交前讀到的值都是相同的,即 V1 和 V2 的值都是 1,當 A 事務提交後,再次查詢時,事務 B 的修改就能被 A 看到了,因此 V3 的值爲 2。spa

在「串行化」的隔離級別下,當事務 B 在 t4 時刻執行更新時,因爲與事務 A 操做的是同一行,且出現讀寫衝突,此時事務 B 被會阻塞,等待事務 A 執行完畢後,再執行事務 B,因此 V1 和 V2 的值是 1,V3 的值是 2。3d

MVCC

更新操做

在數據庫表的記錄中,每個記錄都會添加三個字段:

  • DBTRXID:6個字節,表示最近一次修改本記錄的事務ID
  • DBROLLPTR :7 個字節,回滾指針,指向回滾段中的 undo log record,用於找出這個記錄的上個修改版本的數據。
  • DBROWID:6 個字節,一個單調遞增的 ID,肯定表中記錄的惟一性。

當對某個記錄進行更新時,會將當前記錄寫入 undo log 中,並更新當前記錄中 DBROLLPTR 字段值,使其指向剛纔的 undo log record,而後更新當前記錄相關字段值,同時更新 DBTRXID 字段,記錄執行更新操做的事務 ID。簡略的更新過程大體以下所示

mysql-mvcc-update

查詢操做

由上面的更新操做能夠得知,數據庫表記錄始終記錄着最新的更新結果,那對於「可重複讀」和「讀提交」的隔離級別的事務,它是如何保證在開啓本事務後,其餘事務對記錄進行了更新操做,而本事務仍然可以讀取到準確的值(不是表記錄的最新值,而是歷史版本的值)的?從更新操做中能夠得知,經過循環遍歷 DBROLLPTR 能夠拿到當前記錄的歷史版本(固然,只是活躍的事務,若是當前記錄沒有相關事務在操做,則會清理 undo log,就不能拿到歷史版本數據了) 。可是這麼多歷史版本的數據,究竟哪一個版本的數據纔是當前事務所要的呢?這時就要判斷當前版本的數據是否對當前事務可見了。

在開啓事務時,會將當前活躍的事務(已經開啓了事務,可是尚未提交)的事務 ID 放在一個數組裏面,同時記錄數組裏面最小的事務 ID 爲「低水位」,記錄當前系統已經建立的事務ID 的最大值加一爲「高水位」。這三者組成了一個事務的一致性視圖(read-view)。當事務要查詢某個記錄的數據時,實際上就是拿該記錄的事務ID(包括歷史版本的事務ID)和這個一致性視圖進行比較,直到某個版本的數據是可見的爲止。其查詢過程以下:

  • 讀取的記錄的事務ID小於低水位,說明這個版本的數據在開啓本事務前已經提交,是可見的,直接返回這個數據
  • 讀取的記錄的事務ID大於高水位,說明這個版本的數據在開啓本事務後提交的,不可見,從記錄中取出 DBROLLPTR 指向的記錄並讀取其事務 ID,開始下一輪的判斷
  • 讀取的記錄的事務ID介於低水位和高水位中間,此時判斷事務ID是否在一致性視圖的事務數組中:
    • 若是不在,說明這個版本的數據在開啓本事務前已經提交,是可見的,直接返回這個數據
    • 若是在,說明這個版本的數據是由開啓事務後的其餘活躍事務提交的,對本事務是不可見的,所以須要從記錄中取出 DBROLLPTR 指向的記錄並讀取其事務 ID,開始下一輪的判斷

其判斷過程的流程圖大體以下所示file

關於判斷數據可見性,除了上述用高水位、低水位和事務視圖數組結合判斷以外,能夠簡化成如下規則判斷:

  • 對於當前事務中的數據,可見
  • 對於其餘事務中的數據
    • 若是版本未提交,不可見
    • 若是版本已經提交,且是在建立本事務視圖後提交的,不可見
    • 若是版本已經提交,且是在建立本事務視圖前提交的,可見

例子

如今用一個例子(此例子來自:林曉斌:MySQL實戰45講)來對上述查找過程進行說明。假設在「可重複讀」的隔離級別下,有如下的表結構和數據。

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);複製代碼

假設進行如下的操做(事務C 的 update 操做完即自動提交事務),在進行如下操做前,假設當前活躍的事務 ID 爲 99,記錄(1,1)的 DBTRXID 值是 90。則事務 A 的視圖數組是 [99, 100],事務 B 的視圖數組是 [99, 100, 101],事務 C 的視圖數組是 [99, 100, 101, 102]

事務A(事務ID:100)
事務B(事務ID:101)
事務C(事務ID:102)
start transaction with consistent snapshot;


start transaction with consistent snapshot;


update t set k = k + 1 where id = 1;

update t set k = k + 1 where id = 1;


select k from t where id = 1;

select k from t where id = 1;


commit;



commit;

當事務 A 執行查詢語句時,其查詢數據邏輯圖(此圖來自:林曉斌:MySQL實戰45講)以下所示

mysql-mvcc-query

其查找過程以下,首先,獲取記錄的事務ID(101),比高水位大,不可見,因此取出記錄的上一個歷史版本,獲取其事務ID(102),比高水位大,不可見,再獲取記錄的上一個歷史版本,獲取其事務ID(90),比低水位小,可見,因此返回這個記錄中的 k 字段的值 1。

固然,也能夠用簡化版原本判斷。過程以下,首先,獲取記錄(1,3),尚未提交,不可見,取出上一個歷史版本(1,2),(1,2)已經提交,可是在本事務視圖建立後提交的,不可見,繼續取出上一個歷史版本(1,1),(1,1)已經提交,且是在本事務視圖建立前提交的,可見,因此最終返回 k 的值是 1。

此處須要額外關注的是,事務 B 的更新操做,是在當前記錄的最新值上更新的,並非在歷史數據上更新的,不然會丟失事務 B 的更新操做。其實,更新數據都是先讀後寫的,並且這個讀,是讀的當前值,稱爲「當前讀」。

若是是在「讀提交」的隔離級別下,處理邏輯相似,只是生成一致性視圖的狀況不一樣:

  • 在「可重複度」隔離級別下,只須要在事務開始的時間建立一致性視圖,以後事務裏的其餘查詢都共用這個一致性視圖
  • 在「讀提交」隔離級別下,每個語句執行前都會從新算出一個新的視圖

因此上述例子,若是是在「讀提交」隔離級別下,事務 A 在執行查詢語句時,會建立新的一致性視圖,此時一致性視圖中的活躍事務ID數組是 [99, 100, 101],其查找過程以下,讀取當前記錄事務 ID(101),在視圖數組中,不可見,取出上一個歷史版本記錄,讀取事務ID(102),介於低水位和高水位之間,且不在視圖數組中,可見,因此返回記錄的 k 值 2。

其餘

  • 四種隔離級別,只有「讀提交」和「可重複度」兩個隔離級別可以使用 MVCC,所以也只有這兩個隔離級別會建立一致性視圖(read-view)。由於「讀提交」隔離級別下每次都是讀取的最新記錄,因此不用 MVCC,也不用建立一致性視圖;「串行化」隔離級別,則是用加鎖方式來實現併發的,也不用 MVCC ,因此也不用建立一致性視圖。關於「可重複度」和「讀提交」兩個隔離級別下一致性視圖的差異,主要體如今:「可重複度」隔離級別下的一致性視圖是在啓動事務時建立的,建立後,本事務共用一個視圖;而「可讀提交」隔離級別下的一致性視圖是在執行 SQL 時建立的,每個 SQL 都會單首創建一個視圖,並不會共用。
  • 當前讀(current read),每次讀取的都是記錄的最新數據,主要包含如下 SQL 語句
    • select ... lock in share mode
    • select ... for update
    • insert
    • update
    • delete
  • 快照讀(snapshot read),可能讀取記錄的歷史版本數據,主要用於 MVCC 中的簡單的 select (不包括 select ... lock in share mode,select ... for update),保證事務讀取的一致性。

參考資料

[1] 林曉斌. 事務隔離:爲何你改了我還看不見?[J/OL]. https://time.geekbang.org/column/article/68963 ,2018-11-19

[2] 林曉斌. 事務隔離:事務究竟是隔離的仍是不隔離的?[J/OL]. https://time.geekbang.org/column/article/70562 ,2018-11-30

[3] MySQL官方文檔: https://dev.mysql.com/doc/refman/8.0/en/innodb-multi-versioning.html

相關文章
相關標籤/搜索