MySQL 學習筆記(二)MVCC 機制

Ethan_Wong發表於2022-03-05

之前在講 MySQL 事務隔離性提到過,對於寫操作給讀操作的影響這種情形下發生的髒讀、不可重複讀、虛讀問題。是通過MVCC 機制來進行解決的,那麼MVCC到底是如何實現的,其內部原理是怎樣的呢?我們要抓住三個方面:記錄中的4個隱藏欄位、undo log 和 read view。

一、MVCC 定義和解決的讀問題

1. 事務併發一致性的讀問題

髒讀(Dirty Read)

髒讀也就是當前事務讀取到了其他事務還未提交的資料。我們舉個例子來看看:

Time session A session B
1 -設定當前會話事務隔離級別為:讀未提交 set session transaction isolation level read uncommitted;
2 -設定當前會話事務隔離級別為:讀未提交 set session transaction isolation level read uncommitted;
3 start transaction; select * from account;
4 start transaction; select * from account; update account set user_name = '孫七' where id = 6;
5 select * from account; 查詢到了session B 中還沒有提交的資料

不可重複讀(Non-Repeatable Read)

不可重複讀是兩次讀取的結果不相同,和髒讀的區別就是不可重複讀讀到了其他事務提交後的資料。

舉個例項來看看:

Time session A session B
1 -設定當前會話事務隔離級別為:讀已提交 set session transaction isolation level read committed;
2 -設定當前會話事務隔離級別為:讀已提交 set session transaction isolation level read committed;
3 start transaction; select * from account;
4 start transaction; select * from account; update account set user_name='趙趙' where id = 1; -此時已經發生修改 select * from account;
5 select * from account;
6 commit;
7 select * from account;對於未提交的事務,查詢不到。相對於前一個隔離級別,杜絕了未提交事務修改對另外會話的影響。一旦另外的會話提交後,在進行查詢時,會查出相應的修改。即在一個完整會話中,前後查詢不同。

虛讀(Phantom)

所謂虛讀,也就是根據某些搜尋條件先後查詢資料庫,發現兩次查詢結果條數不同。和不可重複讀的區別就是不可重複讀的條數沒有變化,虛讀條數因為修改操作造成了條數變化。

下面舉個例項來說明:

Time session A session B
1 -設定當前會話事務隔離級別為:可重複讀 set session transaction isolation level repeatable read; select @@transaction_isolation;
2 -設定當前會話事務隔離級別為:可重複讀 set session transaction isolation level repeatable read; select @@transaction_isolation;
3 start transaction; select * from account;
4 start transaction; select * from account; insert into account values(7,'劉八',100); -此時已經發生修改 select * from account;
5 select * from account;
6 commit;
7 select * from account; insert into account values(7,'劉八',100);雖然此時查詢全表沒有發現新的資料,但是這個時候插入和session B 中相同的插入語句卻提示存在一條 key = 7 的語句,說明 session B 的操作確實影響到了 session A 。 這就是虛讀

2.MVCC的定義

全稱叫 Multi-Version Concurrency Control 的多版本併發控制。也就是指“維持一個資料的多個版本,使得讀寫操作沒有衝突”。

在說明 MVCC 原理前,先了解一下 InnoDB 的當前讀和快照讀:

當前讀

當前讀,也就是它讀取的是記錄的最新版本,而且還要保證其他併發事務不能修改當前記錄,實現方式是對讀取記錄進行加鎖。比如下面給出的都是當前讀

#共享鎖
select lock in share mode;
select for update;
#排他鎖
update
insert
delete

快照讀

快照讀是一種基於多版本併發控制(MVCC)的不加鎖讀取形式,由於多版本控制,使得快照讀讀到的可能不是資料的最新版本。比如不加鎖的select 操作就是快照讀。

二、MVCC 實現原理

1. 記錄的三個隱藏欄位

對於InnoDB 儲存引擎來說,它的每條聚簇索引記錄中都包含有以下三個隱藏欄位:

  • row_id:隱藏主鍵。如果該資料表中沒有設定主鍵,就會自動生成一個6位元組的row_id
  • roll_pointer:回滾指標。 指向舊版本的 undo 日誌
  • trx_id:最近修改記錄的事務ID。記錄建立這條記錄或者最後一次修改該記錄的事務ID

如圖所示,row_id 表示該記錄生成的唯一隱式主鍵;trx_id 表示當前操作該記錄的事務ID;roll ptr 是指向上一版本的 undo 日誌的地址。

2. undo 日誌

undo log 就是回滾日誌,之前在事務的原子性中介紹過,它是保證事務原子性的機制。undo 日誌儲存的只有 insertdeleteupdate這些修改記錄的操作。下面舉個例子來幫助理解 undo log 的執行流程:

  • 1.有一個事務編號為1 的事務向資料表中插入一條記錄,此時事務的狀態是:

    • row_id:隱藏主鍵為1
    • trx_id:建立該記錄的事務ID
    • roll ptr:其上個版本的 undo 日誌為空
  • 2.第二個事務編號為2的事務對該記錄進行修改,將name 欄位的 ethan 改為 bob。此時的操作有:

    • 修改資料時,資料庫會對該行加排他鎖
    • 把該行資料拷貝一份到 undo log 中
    • 拷貝完成後,再修改該記錄name 欄位的 ethan 為 bob、修改隱藏欄位的事務ID 為2,回滾指標指向拷貝到 undo log 的記錄。
    • 事務提交後釋放排他鎖

  • 3.若第三個事務ID 為 3 對記錄的age 欄位進行了修改,將 20 修改為 18,則會出現:

    • 事務3修改記錄時,資料庫對該行加排他鎖
    • 資料庫將該行資料拷貝到 undo log 中
    • 拷貝完畢後將該記錄欄位的 age 改成 18。修改隱藏事務ID 為 3,回滾指標指向上個版本的地址
    • 事務提交後釋放鎖

從第二次我們會發現,undo log 中會出現多個版本的日誌。這就是版本鏈。鏈首是最新的舊記錄,鏈尾是最早的舊記錄。

3. ReadView(讀檢視)

ReadView 定義

ReadView 是事務進行快照讀那一刻,生成的一個資料系統當前的快照,記錄並維護當前活躍事務的id,並且這個 ID 值是遞增的。ReadView 的作用就是用來做可見性判斷,記錄當前事務執行快照讀時,建立的ReadView 能夠看到哪些版本的資料。

那麼是ReadView 是怎麼判斷的呢?

ReadView 版本可見性判斷規則

在ReadView 檢視中主要有四個重要的屬性:

  • trx_list: 一個數值列表,當前系統活躍的讀寫事務的事務id 列表
  • min_trx_id: trx_list 中最小的事務id,trx_list 中的最小值
  • max_trx_id: 不是trx_list 的最大值,它是指系統應該分配給下一事務的事務id
    • 比如現在 trx_list 中有id 為1、2、3、4的事務,那麼max_trx_id 的值就是5
  • creator_trx_id:生成該 ReadView 事務的事務ID

在訪問某條記錄時,只需要按照下面的步驟來判斷記錄的某個版本是否可見:

  • 1.(trx_id == creator_trx_id)若被訪問版本的trx_id值與當前 ReadView 中的 creator_trx_id 相同,也就是說當前事務在訪問它自己修改過的記錄,該版本可以被當前事務訪問。
  • 2.(trx_id < min_trx_id)若被訪問版本的trx_id 值小於 ReadView 的 min_trx_id 值,表明生成該版本的事務在當前事務生成ReadView 以前已經提交,該版本可以被當前事務訪問。
  • 3.(trx_id >=max_trx_id)若被訪問版本的trx_id 值大於或等於 ReadView 中的 max_trx_id ,表明生成該版本的事務在當前事務生成 ReadView 後才開啟,該版本可以被當前事務訪問。
  • 4.(min_trx_id <trx_id < max_trx_id)若被訪問版本的trx_id 值介於 ReadView 的 min_trx_idmax_trx_id 值之間,需要判斷trx_id 屬性值是否存在 trx_list
    • 如果存在,說明建立 ReadView 時生成該版本的事務還是活躍的,該版本不可以被訪問
    • 如果不存在,說明建立 ReadView 時生成該版本的事務已經被提交,因此該版本可以被訪問

如果某個版本的資料對當前事務是不可見的,那就順著版本鏈找到下一個版本資料,繼續執行上面的步驟來判斷記錄的可見性,依次類推。知道版本中的最後一個版本。如果記錄的最後一個版本也不可見,意味著該條記錄對當前事務完全不可見,查詢結果就不包含該記錄。

舉例

下面讓我們來看看 MVCC 實現的具體流程是怎樣的,如下表是事務ID 為2 的事務對某行資料執行了快照讀,其中的列表如下:

事務1 事務2 事務3 事務4
事務開始 事務開始 事務開始 事務開始
修改且已提交
進行中 快照讀 進行中

那麼此時ReadView 的引數值為:

  • trx_list:事務1、2、3
  • min_trx_id:事務1
  • max_trx_id:事務5
  • creator_trx_id:事務2

以事務4 版本為例,我們經過上述規則來比較看當前ReadView 能否看見事務4版本的資料:

  • 經比較,只有第四條規則滿足。此時trx_id 的值是介於min_trx_idmax_trx_id 之間,但是不在 trx_list 中,因此經判斷該事務已經提交。所以該版本可以被訪問。

其實這個規則很好理解,在活躍事務列表裡面的,意味還沒有提交,除了建立ReadView 的當前事務,其他的事務都不可見。不在列表裡面的說明都已經提交,自然可以看見。如下圖除了黃色和紅色不可見,其他的版本都可見。

三、MVCC 如何解決髒讀、不可重複讀和虛讀

首先回顧一下MySQL的事務隔離級別中的檢視

  • 讀未提交(RU):它是直接返回記錄的最新值,沒有檢視
  • 讀已提交(RC):每次查詢都會建立一個ReadView
  • 可重複讀(RR):這個ReadView是在事務啟動時建立,整個事務存在期間都用這個ReadView
  • 序列化(serializable):直接用加鎖的方式來避免並行訪問

1.MVCC 解決髒讀

在讀已提交的MVCC 中,每次查詢都會建立一個 ReadView 。由於版本控制的可見性規則,使得當前事務只看的到已經提交的資料,所以這樣就避免了看見未提交的資料,從而解決了髒讀。

2.MVCC 解決不可重複讀

因為RC 級別每次查詢都會建立一個 ReadView ,所以對於已提交的事務,由於不能共用一個ReadView ,還是會造成兩次讀取過程中的不可重複讀。所以RR 級別通過使用從啟動到結束使用一個 ReadView, 來解決提交兩次查詢讀取不一致的現象。

3.MVCC 到底能不能解決虛讀?

先說結論:MVCC可以解決“快照讀”,無法解決“當前讀”

MVCC 可以解決“快照讀”

MVCC 可以解決如不加鎖的select。原理就是MVCC 使用快照來控制版本資料讀取的範圍,從而在 RR 級別避免了虛讀。在我上面講虛讀的舉例就說明了,在select 快照讀時,沒有發現新的資料。但是新插入同樣的資料卻報錯,說明MVCC 無法徹底解決虛讀。

MVCC 無法解決“當前讀”

如果在select 上加鎖,使用“當前讀”,虛讀還是會出現。所以真正要解決虛讀,還是得用加鎖的形式來解決。所以一般而言,也只有序列化級別才能真正解決虛讀。

參考資料

https://www.cnblogs.com/kismetv/p/10331633.html

https://pdai.tech/md/db/sql-mysql/sql-mysql-mvcc.html

https://time.geekbang.org/column/article/68963

https://blog.csdn.net/qq_35590091/article/details/107734005

《MySQL是怎樣執行的-從根兒上理解MySQL》

相關文章