看一遍就懂:MVCC原理詳解

MVCC實現原理也是一道非常高頻的面試題,自己在整理這篇文章的時候,感覺到網上的資料在講這塊知識點上寫的五花八門,好像大家的理解並沒有一致。

這裡將自己所理解的做一個總結,個人會覺得這是一篇含金量挺高的一篇文章(哈哈),所以請你堅持認真的看下去,一定會對你有收穫。

如果文章中哪裡沒有理解,或者認為我講的不對的地方,都歡迎留言一起交流哈。

前言

一些基本概念我這裡不在做闡述了。好比什麼是事務? 事務的ACID? 四大隔離級別?

有關事務並發存在的問題之前有寫過一篇文章:一文詳解臟讀、不可重複讀、幻讀

如果你還不清楚不可重複讀和幻讀的區別,非常建議看完上面這篇文章。因為好多人會把不可重複讀和幻讀搞在一起。

所以會認為MVCC能解決幻讀,其實MVCC解決的不是幻讀,而是不可重複讀,下面會用實際例子來證明這一點。

一、什麼是MVCC

多版本控制: 指的是一種提高並發的技術。最早的資料庫系統,只有讀讀之間可以並發,讀寫,寫讀,寫寫都要阻塞。引入多版本之後,只有寫寫之間相互阻塞,其他三種操作都可以並行,這樣大幅度提高了InnoDB的並發度。

在內部實現中,InnoDB通過undo log保存每條數據的多個版本,並且能夠找回數據歷史版本提供給用戶讀,每個事務讀到的數據版本可能是不一樣的。在同一個事務中,用戶只能看到該事務創建快照之前已經提交的修改和該事務本身做的修改。

MVCC只在已提交讀(Read Committed)和可重複讀(Repeatable Read)兩個隔離級別下工作,其他兩個隔離級別和MVCC是不兼容的。因為未提交讀,總數讀取最新的數據行,而不是讀取符合當前事務版本的數據行。而串列化(Serializable)則會對讀的所有數據多加鎖。

MVCC的實現原理主要是依賴每一行記錄中兩個隱藏欄位,undo log,ReadView

二、MVCC相關的一些概念

這裡我們先來理解下有關MVCC相關的一些概念,這些概念都理解後,我們會通過實際例子來演示MVCC的具體工作流程是怎麼樣的。

1、事務版本號

事務每次開啟時,都會從資料庫獲得一個自增長的事務ID,可以從事務ID判斷事務的執行先後順序。這就是事務版本號。

也就是每當begin的時候,首選要做的就是從資料庫獲得一個自增長的事務ID,它也就是當前事務的事務ID。

2、隱藏欄位

對於InnoDB存儲引擎,每一行記錄都有兩個隱藏列trx_idroll_pointer,如果數據表中存在主鍵或者非NULL的UNIQUE鍵時不會創建row_id,否則InnoDB會自動生成單調遞增的隱藏主鍵row_id。

列名 是否必須 描述
row_id 單調遞增的行ID,不是必需的,佔用6個位元組。 這個跟MVCC關係不大
trx_id 記錄操作該行數據事務的事務ID
roll_pointer 回滾指針,指向當前記錄行的undo log資訊

這裡的記錄操作,指的是insert|update|delete。對於delete操作而已,InnoDB認為是一個update操作,不過會更新一個另外的刪除位,將行表示為deleted,並非真正刪除。

3、undo log

undo log可以理解成回滾日誌,它存儲的是老版本數據。在表記錄修改之前,會先把原始數據拷貝到undo log里,如果事務回滾,即可以通過undo log來還原數據。或者如果當前記錄行不可見,可以順著undo log鏈找到滿足其可見性條件的記錄行版本。

在insert/update/delete(本質也是做更新,只是更新一個特殊的刪除位欄位)操作時,都會產生undo log。

在InnoDB里,undo log分為如下兩類:

1)insert undo log : 事務對insert新記錄時產生的undo log, 只在事務回滾時需要, 並且在事務提交後就可以立即丟棄。

2)update undo log : 事務對記錄進行delete和update操作時產生的undo log,不僅在事務回滾時需要,快照讀也需要,只有當資料庫所使用的快照中不涉及該日誌記錄,對應的回滾日誌才會被刪除。

undo log有什麼用途呢?

1、事務回滾時,保證原子性和一致性。
2、如果當前記錄行不可見,可以順著undo log鏈找到滿足其可見性條件的記錄行版本(用於MVCC快照讀)。

4、版本鏈

多個事務並行操作某一行數據時,不同事務對該行數據的修改會產生多個版本,然後通過回滾指針(roll_pointer),連成一個鏈表,這個鏈表就稱為版本鏈。如下:

5、快照讀和當前讀

快照讀: 讀取的是記錄數據的可見版本(有舊的版本)。不加鎖,普通的select語句都是快照讀,如:

select * from user where id = 1;

當前讀:讀取的是記錄數據的最新版本,顯式加鎖的都是當前讀

select * from user where id = 1 for update;
select * from user where id = 1 lock in share mode;

6、ReadView

ReadView是事務在進行快照讀的時候生成的記錄快照, 可以幫助我們解決可見性問題的

如果一個事務要查詢行記錄,需要讀取哪個版本的行記錄呢? ReadView 就是來解決這個問題的。 ReadView 保存了當前事務開啟時所有活躍的事務列表。換個角度,可以理解為: ReadView 保存了不應該讓這個事務看到的其他事務 ID 列表。

ReadView是如何保證可見性判斷的呢?我們先看看 ReadView 的幾個重要屬性

  • trx_ids: 當前系統中那些活躍(未提交)的讀寫事務ID, 它數據結構為一個List。(重點注意:這裡的trx_ids中的活躍事務,不包括當前事務自己和已提交的事務,這點非常重要)

  • low_limit_id: 目前出現過的最大的事務ID+1,即下一個將被分配的事務ID。

  • up_limit_id: 活躍事務列表trx_ids中最小的事務ID,如果trx_ids為空,則up_limit_id 為 low_limit_id。

  • creator_trx_id: 表示生成該 ReadView 的事務的 事務id

訪問某條記錄的時候如何判斷該記錄是否可見,具體規則如下:

  • 如果被訪問版本的 事務ID = creator_trx_id,那麼表示當前事務訪問的是自己修改過的記錄,那麼該版本對當前事務可見;
  • 如果被訪問版本的 事務ID < up_limit_id,那麼表示生成該版本的事務在當前事務生成 ReadView 前已經提交,所以該版本可以被當前事務訪問。
  • 如果被訪問版本的 事務ID > low_limit_id 值,那麼表示生成該版本的事務在當前事務生成 ReadView 後才開啟,所以該版本不可以被當前事務訪問。
  • 如果被訪問版本的 事務ID在 up_limit_id和m_low_limit_id 之間,那就需要判斷一下版本的事務ID是不是在 trx_ids 列表中,如果在,說明創建 ReadView 時生成該版本的事務還是活躍的,該版本不可以被訪問;
    如果不在,說明創建 ReadView 時生成該版本的事務已經被提交,該版本可以被訪問。

畫張圖來理解下

這裡需要思考的一個問題就是 何時創建ReadView?

上面說過,ReadView是來解決一個事務需要讀取哪個版本的行記錄的問題的。那麼說明什麼?只有在select的時候才會創建ReadView。但在不同的隔離級別是有區別的:

在RC隔離級別下,是每個select都會創建最新的ReadView;而在RR隔離級別下,則是當事務中的第一個select請求才創建ReadView(下面會詳細舉例說明)。

那insert/update/delete操作呢?

這樣操作不會創建ReadView。但是這些操作在事務開啟(begin)且其未提交的時候,那麼它的事務ID,會存在在其它存在查詢事務的ReadView記錄中,也就是trx_ids中。

三、MVCC實現原理分析

1、如何查詢一條記錄

  1. 獲取事務自己事務ID,即trx_id。(這個也不是select的時候獲取的,而是這個事務開啟的時候獲取的 也就是begin的時候)
  2. 獲取ReadView(這個才是select的時候才會生成的)
  3. 資料庫表中如果查詢到數據,那就到ReadView中的事務版本號進行比較。
  4. 如果不符合ReadView的可見性規則, 即就需要Undo log中歷史快照,直到返回符合規則的數據;

InnoDB 實現MVCC,是通過ReadView+ Undo Log 實現的,Undo Log 保存了歷史快照,ReadView可見性規則幫助判斷當前版本的數據是否可見。

2、MVCC是如何實現讀已提交和可重複讀的呢?

其實其它流程都是一樣的,讀已提交和可重複讀唯一的區別在於:在RC隔離級別下,是每個select都會創建最新的ReadView;而在RR隔離級別下,則是當事務中的第一個select請求才創建ReadView。

看完下面這個例子你應該就明白了。


四、經典面試題:MVCC能否解決了幻讀問題呢?

有關這個問題查了很多資料,有的說能解決,有的說不能解決,也有人說能解決部分幻讀場景。這裡部分解決指的是能解決快照讀的幻讀問題,不能解決當前讀的幻讀問題。

具體可以看下面這篇文章

面試題之:MVCC能否解決幻讀?//blog.csdn.net/qq_35590091/article/details/107734005

先說我的結論:

MVCC能解決不可重複讀問題,但是不能解決幻讀問題,不論是快照讀和當前讀都不能解決。RR級別解決幻讀靠的是鎖機制,而不是MVCC機制。

既然網上那麼多人說,MVCC解決能解決快照讀下的幻讀問題, 那這裡通過舉示例來說明,MVCC解決不了快照讀的幻讀問題。

假設有張用戶表,這張表的 id 是主鍵。表中一開始有4條數據。

這裡是在RR級別下研究(可重複讀)。

1、事務A,查詢是否存在 id=5 的記錄,沒有則插入,這是我們期望的正常業務邏輯。

2、這個時候 事務B 新增的一條 id=5 的記錄,並提交事務。

3、事務A,再去查詢 id=5 的時候,發現還是沒有記錄。

上面的文章是這樣來舉例說明,事務A第一次和第二次讀到的是一樣的,所以認為解決了幻讀。我不認為這個是解決了幻讀,而是解決了不可能重複讀。它保證了第一次和第二次所讀到的結果是一樣的。

解決幻讀了嗎?顯然沒有,因為這個時候如果事務A執行一條插入操作

INSERT INTO `user` (`id`, `name`, `pwd`) VALUES (5, '田七', 'fff');

最終 事務A 提交事務,發現報錯了。這就很奇怪,查的時候明明沒有這條記錄,但插入的時候 卻告訴我 主鍵衝突,這就好像幻覺一樣。這才是幻讀問題。

所以說MVCC是不能解決的,要想解決還是需要鎖。

這裡事務A能正常的插入的前提就是其它事務不能插入id=5並提交成功。要解決這個問題也很簡單,就是事務A先獲得id=5這個排它鎖。

我們可以在事務A第一次查詢的時候加一個排他鎖

select *  from `user` where id = 5 for update

那麼事務B的插入動作永遠屬於堵塞狀態,直到事務A插入成功,並提交。那麼最終是事務B報主鍵衝突而回滾。但事務A不會因為查詢的時候沒有這條記錄,插入失敗。也就解決了幻讀問題。

所以說 RR級別下解決幻讀問題靠的是鎖機制,而不是MVCC機制。

如果有不認同我的觀點的,可以留下你的觀點,我們可以一起討論交流哈。