MVCC (multiversion concurrency control),多版本併發控制,主要是經過在每一行記錄中增長三個字段,與 undo log 中相關記錄配合使用,同時加上可見性算法,使得各個事務能夠在不加鎖的狀況下可以同時地讀取到某行記錄上的準確值(這個值對不一樣的事務而言多是不一樣的)。使用 MVCC,在不加鎖的狀況下也能讀取到準確的數據,大大提升了併發效率。html
提到 MVCC,必須提到事務。關於事務,有四個特性,即咱們常說的 ACID。mysql
而事務隔離性又分爲四種級別:讀未提交(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
在數據庫表的記錄中,每個記錄都會添加三個字段:
當對某個記錄進行更新時,會將當前記錄寫入 undo log 中,並更新當前記錄中 DBROLLPTR 字段值,使其指向剛纔的 undo log record,而後更新當前記錄相關字段值,同時更新 DBTRXID 字段,記錄執行更新操做的事務 ID。簡略的更新過程大體以下所示
由上面的更新操做能夠得知,數據庫表記錄始終記錄着最新的更新結果,那對於「可重複讀」和「讀提交」的隔離級別的事務,它是如何保證在開啓本事務後,其餘事務對記錄進行了更新操做,而本事務仍然可以讀取到準確的值(不是表記錄的最新值,而是歷史版本的值)的?從更新操做中能夠得知,經過循環遍歷 DBROLLPTR 能夠拿到當前記錄的歷史版本(固然,只是活躍的事務,若是當前記錄沒有相關事務在操做,則會清理 undo log,就不能拿到歷史版本數據了) 。可是這麼多歷史版本的數據,究竟哪一個版本的數據纔是當前事務所要的呢?這時就要判斷當前版本的數據是否對當前事務可見了。
在開啓事務時,會將當前活躍的事務(已經開啓了事務,可是尚未提交)的事務 ID 放在一個數組裏面,同時記錄數組裏面最小的事務 ID 爲「低水位」,記錄當前系統已經建立的事務ID 的最大值加一爲「高水位」。這三者組成了一個事務的一致性視圖(read-view)。當事務要查詢某個記錄的數據時,實際上就是拿該記錄的事務ID(包括歷史版本的事務ID)和這個一致性視圖進行比較,直到某個版本的數據是可見的爲止。其查詢過程以下:
其判斷過程的流程圖大體以下所示
關於判斷數據可見性,除了上述用高水位、低水位和事務視圖數組結合判斷以外,能夠簡化成如下規則判斷:
如今用一個例子(此例子來自:林曉斌: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講)以下所示
其查找過程以下,首先,獲取記錄的事務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。
[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