揭秘MySQL多版本并發(fā)控制實現(xiàn)原理
MySQL 中多版本并發(fā)控制(MVCC),是現(xiàn)代數(shù)據(jù)庫引擎實現(xiàn)中常用的處理讀寫沖突的手段,MVCC 作為 MySQL 高級應(yīng)用特性,目的在于提高數(shù)據(jù)庫高并發(fā)場景下的吞吐性能。
一、MVCC出現(xiàn)背景是什么?
-
臟讀:一個事務(wù)讀取到了另外一個事務(wù)沒有提交的數(shù)據(jù);
-
不可重復(fù)讀: 在同一事務(wù)中,兩次讀取同一數(shù)據(jù),得到內(nèi)容不同;
-
幻讀: 同一事務(wù)中,用同樣的操作讀取兩次,得到的記錄數(shù)不相同。
在 MySQL 中, 默認的隔離級別是可重復(fù)讀 ,可以解決臟讀和不可重復(fù)讀的問題,但不能解決幻讀問題。如果我們想要解決幻讀問題,就需要采用串行化的方式,也就是將隔離級別提升到最高,但這樣一來就會大幅降低數(shù)據(jù)庫的事務(wù)并發(fā)能力。
而MVCC就是通過樂觀鎖的方式來解決不可重復(fù)讀和幻讀問題,它可以在大多數(shù)情況下替代行級鎖,降低系統(tǒng)的開銷。
MySQL 并發(fā)事務(wù)會引起更新丟失問題,解決辦法是鎖,主要分兩類:
-
樂觀鎖:
其實現(xiàn)如同它的名字一樣,是假設(shè)比較好的情況。
每次取數(shù)據(jù)的時候都認為他人不會對其修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號機制和CAS算法實現(xiàn)。
-
悲觀鎖:
悲觀鎖也如同它的名字一樣,總是假設(shè)比較壞的情況,每次取數(shù)據(jù)的時候都認為他人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會阻塞直到它拿到鎖(共享資源每次只給一個線程使用,其它線程阻塞,用完后再把資源轉(zhuǎn)讓給其它線程)。
二、什么是MVCC,它解決了什么問題?
MVCC 是通過數(shù)據(jù)行的多個版本管理來實現(xiàn)數(shù)據(jù)庫的并發(fā)控制,簡單來說它的思想就是保存數(shù)據(jù)的歷史版本。
我們可以通過比較版本號決定數(shù)據(jù)是否顯示出來(具體的規(guī)則后面會介紹到),讀取數(shù)據(jù)的時候不需要加鎖也可以保證事務(wù)的隔離效果。
通過 MVCC 我們可以解決以下幾個問題:
(1)讀寫之間阻塞的問題,通過 MVCC 可以讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,這樣就可以提升事務(wù)并發(fā)處理能力。
(2)降低了死鎖的概率。這是因為 MVCC 采用了樂觀鎖的方式,讀取數(shù)據(jù)時并不需要加鎖,對于寫操作,也只鎖定必要的行。
(3)解決一致性讀的問題。一致性讀也被稱為快照讀,當(dāng)我們查詢數(shù)據(jù)庫在某個時間點的快照時,只能看到這個時間點之前事務(wù)提交更新的結(jié)果,而不能看到這個時間點之后事務(wù)提交的更新結(jié)果。
解釋一下可能難以理解的幾個詞匯:
-
快照讀:
讀取的是快照數(shù)據(jù),不加鎖的簡單的SELECT都屬于快照讀(只是普通的讀操作)。
-
當(dāng)前讀:
當(dāng)前讀就是讀取最新數(shù)據(jù),而不是歷史版本的數(shù)據(jù)。
加鎖的SELECT,或者對數(shù)據(jù)進行增刪改都會進行當(dāng)前讀(包括加鎖的讀取和DML操作)。
三、應(yīng)用舉例分析
為了更好地讓大家理解MVCC,我們用一個示例場景來說明。
假設(shè)有個賬戶金額表 user_balance,包括三個字段,分別是 username 用戶名、balance 余額和 bankcard 卡號,表數(shù)據(jù)如下所示:
用戶 A 和用戶 B 之間進行轉(zhuǎn)賬,此時數(shù)據(jù)庫管理員想要查詢 user_balance 表中的總金額,兩個場景存在并發(fā)情況,在沒有MVCC的情況下,會出現(xiàn)哪些問題呢。
Case1 :因為需要采用加行鎖的方式,用戶 A 給 B 轉(zhuǎn)賬時間等待很久,如下圖所示。
Case2 :當(dāng)我們讀取的時候用了加行鎖,可能會出現(xiàn)死鎖的情況,如下圖所示。
比如當(dāng)我們讀到 A 有 1000 元的時候,此時 B 開始執(zhí)行給 A 轉(zhuǎn)賬。
四、InnoDB如何實現(xiàn)MVCC?
當(dāng)查詢一條記錄的時候,執(zhí)行流程如下:
-
首先獲取事務(wù)自己的版本號,也就是事務(wù) ID;
-
獲取 Read View;
-
查詢得到的數(shù)據(jù),然后與 Read View 中的事務(wù)版本號進行比較;
-
如果不符合 ReadView 規(guī)則,就需要從 Undo Log 中獲取歷史快照;
-
最后返回符合規(guī)則的數(shù)據(jù)。
相關(guān)概念
1. 事務(wù)版本號
一個自增長的事務(wù)ID,用于標記事務(wù)執(zhí)行的先后順序。
2. Read View
在 MVCC 機制中,多個事務(wù)對同一個行記錄進行更新會產(chǎn)生多個歷史快照,這些歷史快照保存在 Undo Log 里。如果一個事務(wù)想要查詢這個行記錄,需要讀取哪個版本的行記錄呢?
這時就需要用到 Read View 了,它幫我們解決了行的可見性問題。Read View 保存了當(dāng)前事務(wù)開啟時所有活躍(還沒有提交)的事務(wù)列表,換個角度,可以理解為 Read View 保存了不應(yīng)該讓這個事務(wù)看到的其他的事務(wù) ID 列表。
Read VIew 中的幾個重要屬性:
-
up_limit_id,活躍的事務(wù)中最小的事務(wù) ID;
-
trx_ids,系統(tǒng)當(dāng)前正在活躍的事務(wù) ID 集合;
-
low_limit_id,活躍的事務(wù)中最大的事務(wù) ID;
-
creator_trx_id,創(chuàng)建這個 Read View 的事務(wù) ID。
3. 行記錄的隱藏列
InnoDB 的葉子節(jié)點段存儲了數(shù)據(jù)頁,數(shù)據(jù)頁中保存了行記錄,在這些行記錄中有一些重要的隱藏字段:
-
DB_ROW_ID :
6-byte,記錄操作該數(shù)據(jù)事務(wù)的事務(wù)ID;
-
DB_TRX_ID :
6-byte,當(dāng)創(chuàng)建表沒有合適的索引作為聚集索引時,會用該隱藏ID創(chuàng)建聚集索引;
-
DB_ROLL_PTR :
7-byte,回滾指針,指向上一個版本數(shù)據(jù)在undo log 里的位置指針;
4. 聚集索引
聚集索引是指數(shù)據(jù)庫表行中數(shù)據(jù)的物理順序與鍵值的邏輯(索引)順序相同。一個表只能有一個聚集索引,因為一個表的物理順序只有一種情況,所以,對應(yīng)的聚集索引只能有一個。
5. Undo Log
InnoDB 將行記錄快照保存在 Undo Log,可以在回滾段中找到它們,主要用于記錄數(shù)據(jù)被修改之前的日志,在對表信息做修改之前先會把數(shù)據(jù)拷貝到Undo Log里,當(dāng)事務(wù)進行回滾時可以通過Undo Log里的日志進行數(shù)據(jù)還原。
回滾段中回滾指針間關(guān)聯(lián)關(guān)系,如下圖所示:
五、InnoDB是如何解決幻讀的?
1、在讀已提交的情況下,即使采用了 MVCC 方式也會出現(xiàn)幻讀
我們同時開啟事務(wù) A 和事務(wù) B,先在事務(wù) A 中進行某個條件范圍的查詢,讀取的時候采用排它鎖,在事務(wù) B 中增加一條符合該條件范圍的數(shù)據(jù),并進行提交,然后我們在事務(wù) A 中再次查詢該條件范圍的數(shù)據(jù),就會發(fā)現(xiàn)結(jié)果集中多出一個符合條件的數(shù)據(jù),這樣就出現(xiàn)了幻讀。
出現(xiàn)幻讀的原因是在讀已提交的情況下,InnoDB 只采用記錄鎖(Record Locking)。
InnoDB 三種行鎖的方式:
-
記錄鎖:
針對單個行記錄添加鎖。
-
間隙鎖(Gap Locking):
可以鎖住一個范圍(索引之間的空隙),但不包括記錄本身。
采用間隙鎖的方式可以防止幻讀情況的產(chǎn)生。
-
Next-Key 鎖:
鎖住一個范圍,同時鎖定記錄本身,相當(dāng)于間隙鎖 + 記錄鎖,可以解決幻讀的問題。
2、在可重復(fù)讀的情況下,InnoDB 可以通過 Next-Key 鎖 +MVCC 來解決幻讀問題。
想插入球員艾利克斯·倫(身高 2.16 米)的時候,事務(wù) B 會超時,無法插入該數(shù)據(jù)。
這是因為采用了 Next-Key 鎖,會將 height>2.08 的范圍都進行鎖定,就無法插入符合這個范圍的數(shù)據(jù)了。然后事務(wù) A 重新進行條件范圍的查詢,就不會出現(xiàn)幻讀的情況。
六、總結(jié)
MVCC 的核心就是 Undo Log+ Read View。
-
“MV”就是通過 Undo Log 來保存數(shù)據(jù)的歷史版本,實現(xiàn)多版本的管理;
-
“CC”是通過 Read View 來實現(xiàn)管理,通過 Read View 原則來決定數(shù)據(jù)是否顯示。
同時針對不同的隔離級別,Read View 的生成策略不同,也就實現(xiàn)了不同的隔離級別。