mvcc的兩種層次的理解

Wayne_Kdl發表於2019-12-08

mvcc是什麼

百度百科:Multi-Version Concurrency Control 多版本併發控制,MVCC是一種併發控制的方法,一般在資料庫管理系統中,實現對資料庫的併發訪問。

一種理解

《高效能mysql》Page12
注:這裡說的都是Repeatable Read
可以認為mvcc是行級鎖的一個變種,但是他在很多情況下避免了加鎖操作,因此開銷更低。
mvcc的實現是通過儲存資料在某個時間點的快照實現的。也就是說,不管需要執行多長時間,每個事務看到的資料都是一致的根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的資料可能是不一樣的

InnoDb的簡化版行為:

InnoDb的mvcc,是通過在每行記錄後面儲存兩個隱藏的列來實現的。這兩個列,一個儲存了行的建立時間,一個儲存行的過期時間(或刪除時間)。當然儲存的不是實際的時間值,而是系統版本號。每開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作為事務的版本號,用來和查詢到的每行記錄的版本號進行比較。下面看一下,在repeatable read下,mvcc具體是怎麼操作的。

SELECT:

根據以下兩個條件檢查每行記錄:
a:只查詢版本早於當前事務版本的資料行(也就是,行的系統版本號小於或者等於事務的系統版本號),這樣可以確保事務讀取的行,要麼是在事務開始之前已經存在的,要麼是事務自身插入或者修改過的。
b:行的刪除版本要麼未定義,要麼大於當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。

INSERT:

為新插入的每一行儲存當前系統版本號作為行版本號。

DELETE:

為刪除的每一行儲存當前系統版本號作為行刪除標識。

UPDATE:

插入一行新紀錄,儲存當前系統版本號作為行版本號,同時儲存當前系統版本號到原來的行作為行刪除標識。

另一種理解

掘金小冊《MySQL 是怎樣執行的:從根兒上理解 MySQL》

基礎知識:

undo日誌:

每對記錄做改動時,需要把回滾所需的東西記錄下來,比如把記錄的舊值記下來。這些為了回滾而記錄的東西稱為undo日誌。

trx_id:

行記錄中對這個聚簇索引記錄做改動的語句所在的事務的事務ID。

roll_pointer:

本質就是一個指標,指向記錄對應的undo日誌。

undo頁面連結串列:

對每條記錄進行改動前,都需要記錄undo日誌,所以在事務執行過程中可能產生很多的undo日誌,因此用連結串列來存放。

mvcc版本鏈:

記錄的每次更新,都會將舊值放入一條undo日誌中,隨著更新次數的增多,所有的版本都會被roll_pointer連線成一個連結串列,稱之為版本鏈,版本鏈的頭節點就是當前記錄最新的值。
例子:
我們現在有這張表,開啟兩個事務,分別做一些操作。

mvcc的兩種層次的理解
mvcc的兩種層次的理解

此刻,版本鏈如圖:

mvcc的兩種層次的理解
由於需要判斷版本鏈中哪個版本是當前事務可見的,引出一個概念:

ReadView:

m_ids:生成ReadView時系統中活躍的事務列表。
min_trx_id: m_ids中的最小值。
max_trx_id: 生成ReadView時系統應該分給下一個事務的id值。
creator_trx_id: 生成ReadView的事務的事務id。 (只有增,刪,改時才會分配事務ID,只讀的化,此值為0)

版本鏈的每個節點的可見判斷原則

1:如果被訪問記錄的trx_id與creator_trx_id相同,即當前事務在訪問它自己修改過的記錄,可見。
2:記錄的trx_id小於min_trx_id值,表示生成該版本的事務已經提交,可見。
3:記錄的trx_id大於max_trx_id值,代表生成該版本的事務在當前事務之後才開啟,不可見。
4:min_trx_id < trx_id < max_trx_id,判斷trx_id是否在m_ids中,如果在,說明建立ReadView時(注意這裡是ReadView,而不是trx_id)該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明建立ReadView時生成該版本的事務已經提交,該版本可以被訪問。

mvcc的兩種層次的理解

版本鏈整體的原則

根據版本鏈,依次尋找可見的節點,找到了就返回,如果沒有可見的,說明該條記錄對該事務不可見。
到這裡,是不是感覺跟《高效能Mysql》中的mvcc有一點接近了,但是並不一樣?別急,慢慢來,關鍵就在於ReadView的生成規則。

可重複讀(重點,注意與第一種理解對照)

在第一次讀取時生成ReadView

mvcc的兩種層次的理解
版本鏈如圖

mvcc的兩種層次的理解

select分析

第三個事務進行第一個select時,m_ids = [2,3],min_trx_id=2,max_trx_id=4,creator_trx_id=0(因為現在只有讀)。
對於 1 c 2 來說, 2屬於[2,3],不可見。
對於 1 b 2 來說,2屬於[2,3],不可見。
對於1 a 1 來說,1<min_trx_id(即2),可見,因此讀取到的就是a。(這也符合我們對可重複讀的認知:解決了髒讀。因為事務1沒提交,所以不會讀到其他事務未提交的資料)

此刻將事務1提交

mvcc的兩種層次的理解

mvcc的兩種層次的理解

第二次select分析:

複用之前的ReadView,即m_ids=[2,3],min_trx_id=2,max_trx_id=4,creator_trx_id=0
對於 1 e 3 來說,3屬於[2,3],不可見
對於 1 d 3 來說,3屬於[2,3],不可見
對於 1 c 2 來說,2屬於[2,3],不可見
對於 1 b 3 來說,2屬於[2,3],不可見
對於 1 a 1來說,1<min_trx_id,可見,因此讀到的是a 。(這符合我們對可重複讀的認知:解決了不可重複讀。)
總結一下,其實很簡單,可重複讀時,第一次select生成ReadView,根據ReadView可見的規則,本事務Begin前的事務均可見,讀取的時刻之後的事務均不可見,之間的事務如果是自己就可見,如果活躍(即select時其他事務還沒提交)不可見,不活躍(select時其他事務已提交)可見。
此事務中的非第一次select,其他事務要麼已經提交(第一次讀時就可見),要麼第一次select之後提交(活躍事務,不可見),這樣就達成了目的:可重複讀。

讀已提交

每次讀取資料前都生成ReadView

第一次select分析

這時的過程跟可重複讀一模一樣。

此刻將事務1提交

mvcc的兩種層次的理解
mvcc的兩種層次的理解

第二次select分析:

第二次select,重新生成ReadView,m_ids=[3],min_trx_id=3,max_trx_id=4,creator_trx_id=0;(其實就是min_trx_3由2變成3,m_ids不包括2了)
對於 1 e 3 來說,3屬於[3],不可見
對於 1 d 3 來說,3屬於[3],不可見
對於 1 c 2 來說,2<3,可見
因此讀到的是c,這也符合我們對讀已提交的理解,第一個事務已經提交了,自然可以看到他提交的c了。

讀未提交

讀取記錄的最新版本即可

序列化

通過加鎖的方式訪問(以後有空再寫)

總結:面試話術

關於框架,有人跟我說,背背面試題就行,問的就是面試題那些東西,當我研究了一點原始碼後,我發現,面試題的總結還真的就是相當精髓,我總結的還不如直接背面試題來的概括、抽象、精準。而且,自己看+總結,就算當時弄清楚了,面試的時候真的記得住那麼多細節嗎?
就比如隔離級別的幾種表現,ReadView說到底也還是為了實現隔離級別,表現在外界看來,那就是隔離級別。我真的能記得住ReadView的細節嗎?我估計是不可能,目測我也就能記住可重複讀只在第一次select時生成ReadView,讀已提交每次select都生成ReadView吧,這是一種快照讀嗎?這是把當時讀取的結果儲存下來,下次再讀就直接讀快照嗎?從ReadView的角度來看,當然不是,但是,他表現的不就是快照讀嗎?生不生成那個快照是叫不叫快照讀的關鍵嗎?難道其他地方所謂的快照讀也不是我想的那麼簡單嗎?我有點迷茫,這些有什麼意義呢?
但是,回憶起我背誦springmvc執行流程的時候,我想我在面試官面前背書的時候,我的眼神一定透露著迷茫,真正看過springmvc執行流程之後,就算我面試用詞不準確,容易遺漏,但我應該是自信的。也許,這就是學習的意義吧。

附註

感覺學到了東西的,或者感覺我寫的稀巴爛但是想追根朔源想去知識源頭的,強烈建議購買掘金的mysql小冊。
回頭看了下小冊第一章,“還有各位寫部落格的同學,引用的少了叫借鑑,引用的多了就,就有點那個了。希望各位不要大段大段的複製貼上,用自己的話寫出來的知識才是自己的東西。”,汗,我也不知道我這算不算有點那個了,我儘量多用用自己的話,多說說自己的理解吧。
sql那段流程跟小冊一樣的,我打算再寫一篇不一樣流程的具體分析,是一個面試題,儘量明天或者後天完成。

相關文章