MVCC原理分析
1:什么是MVCC
MVCC是英文名稱Multi Version Concurrency Control 的簡稱,就是多版本並發控制。
MVCC可以說實現,讀不加鎖,讀寫不沖突。這個可以大大的提高Mysql的性能。
2:MVCC解決了什么問題
多事務的並發進行一般會造成以下幾個問題:
臟讀: A事務讀取到了B事務未提交的內容,而B事務后面進行了回滾.
不可重復讀: 當設置A事務只能讀取B事務已經提交的部分,會造成在A事務內的兩次查詢,結果竟然不一樣,因為在此期間B事務進行了提交操作.
幻讀: A事務讀取了一個范圍的內容,而同時B事務在此期間插入了一條數據.造成"幻覺".
MVCC在MySQL InnoDB中的實現主要是為了提高數據庫並發性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞並發讀
在並發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫並發讀寫的性能
同時還可以解決臟讀,幻讀,不可重復讀等事務隔離問題,但不能解決更新丟失問題
3: Mysql的事務隔離級別
我們先了解一下數據的隔離級別。
1:RU:讀未提交,就是可以讀取到其他未提交事務中的數據,存在臟讀。
2:RC:讀已提交,只可以讀取到已提交的數據,存在幻讀。
3:RR:不可重復讀,不可以重復讀。
4:SERIALIZABLE:串行讀取,沒有臟讀,幻讀,不可重復讀。每次操作都是加鎖。性能較低
MVCC只在REPEATABLE READ和READ COMMITTED兩個隔離級別下工作。
REPEATABLE READ讀取之前系統版本號的記錄,保證同一個事務中多次讀取結果一致。
REPEATABLE READ隔離級別下,MVCC具體操作:
SELECT操作,InnoDB會根據以下兩個條件檢查每行記錄:
a. InnoDB只查找創建版本號早於或等於當前系統版本號的數據行,這樣可以確保事務讀取的行,要么是在事務開始前已經存在的,要么是事務自身插入或者修改過的。
b. 行的刪除版本號要么未定義,要么大於當前的系統版本號(在當前事務開始之后刪除的)。這可以確保事務讀取到的行,在事務開始之前未被刪除。
READ COMMITTED讀取最新的版本號記錄,就是所有事務最新提交的結果。
其他兩個隔離級別和MVCC不兼容。READ UNCOMMITTED總是讀取最新的數據行,而不是符合當前事務版本的數據行。SERIALIZABLE會對所有讀取的行都加鎖。
3:MVCC的實現原理
InnoDB的存儲引擎,的每個記錄都會存在三個隱藏的鍵,分別是:DATA_TRX_ID、DATA_ROLL_PTR、DB_ROW_ID。
- DATA_TRX_ID:記錄當前記錄的事務ID,大小為6個字節。
- DATA_ROLL_PTR:表示指向該行回滾段(rollback segment)的指針,大小為 7 個字節,InnoDB 便是通過這個指針找到之前版本的數據。該行記錄上所有舊版本,在 undo 中都通過鏈表的形式組織。
- DB_ROW_ID:行標識(隱藏單調自增 ID),大小為 6 字節,如果表沒有主鍵,InnoDB 會自動生成一個隱藏主鍵(
row_id並不是必要的,我們創建的表中有主鍵或者非NULL唯一鍵時都不會包含row_id列)

再undo.log中形成一個這樣的版本鏈。
理論解釋:
這里開創了三個事務,
假設我們最初的數據的事務Id是80,那他的數據結構如圖所示:

假設之后兩個id分別為100、200的事務對這條記錄進行UPDATE操作,操作流程如下:

每次對記錄進行改動,都會記錄一條undo日志,每條undo日志也都有一個roll_pointer屬性(INSERT操作對應的undo日志沒有該屬性,因為該記錄並沒有更早的版本),可以將這些undo日志都連起來,串成一個鏈表,所以現在的情況就像下圖一樣:

ReadView
對於使用READ UNCOMMITTED隔離級別的事務來說,直接讀取記錄的最新版本就好了,對於使用SERIALIZABLE隔離級別的事務來說,使用加鎖的方式來訪問記錄。對於使用READ COMMITTED和REPEATABLE READ隔離級別的事務來說,就需要用到我們上邊所說的版本鏈了,核心問題就是:需要判斷一下版本鏈中的哪個版本是當前事務可見的。所以設計InnoDB的大叔提出了一個ReadView的概念,這個ReadView中主要包含當前系統中還有哪些活躍的讀寫事務,把它們的事務id放到一個列表中,我們把這個列表命名為為m_ids。這樣在訪問某條記錄時,只需要按照下邊的步驟判斷記錄的某個版本是否可見:
-
如果被訪問版本的
trx_id屬性值小於m_ids列表中最小的事務id,表明生成該版本的事務在生成ReadView前已經提交,所以該版本可以被當前事務訪問。 -
如果被訪問版本的
trx_id屬性值大於m_ids列表中最大的事務id,表明生成該版本的事務在生成ReadView后才生成,所以該版本不可以被當前事務訪問。 -
如果被訪問版本的
trx_id屬性值在m_ids列表中最大的事務id和最小事務id之間,那就需要判斷一下trx_id屬性值是不是在m_ids列表中,如果在,說明創建ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明創建ReadView時生成該版本的事務已經被提交,該版本可以被訪問。
如果某個版本的數據對當前事務不可見的話,那就順着版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個版本,如果最后一個版本也不可見的話,那么就意味着該條記錄對該事務不可見,查詢結果就不包含該記錄。
在MySQL中,READ COMMITTED和REPEATABLE READ隔離級別的的一個非常大的區別就是它們生成ReadView的時機不同,我們來看一下。
4:實踐驗證RR,RC隔離級別下的MVCC
RC模式下:
現在通過兩個事務來處理一個數據,解讀一下MVCC的實現原理。這里事務提交方式改為手動,事務隔離級別改為RC

1: 查詢結果為:劉備
2:執行事務100,不提交
我們可以得到再undo.log中的一個這樣的版本鏈

這個時候我們再執行,查詢,可以看到查詢結果

可以看到結果還是劉備。
分析:
-
在執行
SELECT語句時會先生成一個ReadView,ReadView的m_ids列表的內容就是[100]。 -
然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列
c的內容是'張飛',該版本的trx_id值為100,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。 -
下一個版本的列
c的內容是'關羽',該版本的trx_id值也為100,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。 -
下一個版本的列
c的內容是'劉備',該版本的trx_id值為80,小於m_ids列表中最小的事務id100,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列c為'劉備'的記錄。
接下來我們執行操作:提交事務100的操作,同時執行事務200的操作,但是不提交,然后執行查詢操作。

我們可以看到查詢結果,為張飛
分析:這個時候的版本鏈是這樣的

-
在執行
SELECT語句時會先生成一個ReadView,ReadView的m_ids列表的內容就是[200](事務id為100的那個事務已經提交了,所以生成快照時就沒有它了)。 -
然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列
c的內容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。 -
下一個版本的列
c的內容是'趙雲',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。 -
下一個版本的列
c的內容是'張飛',該版本的trx_id值為100,比m_ids列表中最小的事務id200還要小,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列c為'張飛'的記錄。
以此類推,如果之后事務id為200的記錄也提交了,再此在使用READ COMMITTED隔離級別的事務中查詢表t中id值為1的記錄時,得到的結果就是'諸葛亮'了,具體流程我們就不分析了。總結一下就是:使用READ COMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的ReadView。

RR模式下的MVCC
對於使用REPEATABLE READ隔離級別的事務來說,只會在第一次執行查詢語句時生成一個ReadView,之后的查詢就不會重復生成了。我們還是用例子看一下是什么效果。
同樣,按照上面的操作,重新操作。
事務100,操作,但是不提交,事務200 不進行操作,然后查詢,

結果就是這樣,原因和之前的一樣,可以分析。這里就不在分析了。
接下來我們執行查詢,但是查詢事務不提交,然后提交事務100;同時執行事務200,但是不提交;然后再次執行查詢

分析:
上面執行可以得到undo.log的版本鏈

-
因為之前已經生成過
ReadView了,所以此時直接復用之前的ReadView,之前的ReadView中的m_ids列表就是[100, 200]。 -
然后從版本鏈中挑選可見的記錄,從圖中可以看出,最新版本的列
c的內容是'諸葛亮',該版本的trx_id值為200,在m_ids列表內,所以不符合可見性要求,根據roll_pointer跳到下一個版本。 -
下一個版本的列
c的內容是'趙雲',該版本的trx_id值為200,也在m_ids列表內,所以也不符合要求,繼續跳到下一個版本。 -
下一個版本的列
c的內容是'張飛',該版本的trx_id值為100,而m_ids列表中是包含值為100的事務id的,所以該版本也不符合要求,同理下一個列c的內容是'關羽'的版本也不符合要求。繼續跳到下一個版本。 -
下一個版本的列
c的內容是'劉備',該版本的trx_id值為80,80小於m_ids列表中最小的事務id100,所以這個版本是符合要求的,最后返回給用戶的版本就是這條列c為'劉備'的記錄。
也就是說兩次SELECT查詢得到的結果是重復的,記錄的列c值都是'劉備',這就是可重復讀的含義。如果我們之后再把事務id為200的記錄提交了,之后再到剛才使用REPEATABLE READ隔離級別的事務中繼續查找這個id為1的記錄,得到的結果還是'劉備',具體執行過程大家可以自己分析一下。
總結
從上邊的描述中我們可以看出來,所謂的MVCC(Multi-Version Concurrency Control ,多版本並發控制)指的就是在使用READ COMMITTD、REPEATABLE READ這兩種隔離級別的事務在執行普通的SEELCT操作時訪問記錄的版本鏈的過程,這樣子可以使不同事務的讀-寫、寫-讀操作並發執行,從而提升系統性能。READ COMMITTD、REPEATABLE READ這兩個隔離級別的一個很大不同就是生成ReadView的時機不同,READ COMMITTD在每一次進行普通SELECT操作前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操作前生成一個ReadView,之后的查詢操作都重復這個ReadView就好了。
