首頁 > 軟體

mysql中的mvcc 原理詳解

2022-10-26 14:03:21

簡介

MVCC(Multi-Version Concurrency Control)多版本並行控制,是用來在資料庫中控制並行的方法,實現對資料庫的並行存取用的。在MySQL中,MVCC只在讀取已提交(Read Committed)和可重複讀(Repeatable Read)兩個事務級別下有效。其是通過Undo紀錄檔中的版本鏈和ReadView一致性檢視來實現的。MVCC就是在多個事務同時存在時,SELECT語句找尋到具體是版本鏈上的哪個版本,然後在找到的版本上返回其中所記錄的資料的過程。

首先需要知道的是,在MySQL中,會預設為我們的表後面新增三個隱藏欄位:

  • DB_ROW_ID:行ID,MySQL的B+樹索引特性要求每個表必須要有一個主鍵。如果沒有設定的話,會自動尋找第一個不包含NULL的唯一索引列作為主鍵。如果還是找不到,就會在這個DB_ROW_ID上自動生成一個唯一值,以此來當作主鍵(該列和MVCC的關係不大);
  • DB_TRX_ID:事務ID,記錄的是當前事務在做INSERT或UPDATE語句操作時的事務ID(DELETE語句被當做是UPDATE語句的特殊情況,後面會進行說明);
  • DB_ROLL_PTR:回滾指標,通過它可以將不同的版本串聯起來,形成版本鏈。相當於連結串列的next指標。

注意,新增的隱藏欄位並不是很多人認為的建立時間和刪除時間,同時在MySQL中MVCC的實現也不是通過什麼快照來實現的。之所以有這種說法可能是源自於《高效能MySQL》一書中對MySQL中MVCC的錯誤結論,然後就人云亦云傳開了(注意,我這裡一直強調的是MySQL中MVCC的實現,是因為在不同的資料庫中可能會有不同的實現)。所以說看原始碼和看官方檔案才是最權威的解釋)

前言

很多人在談起mysql事務的時候都能很快的答出mysql的幾種事務隔離級別,以及在各自隔離級別下產生的問題,但是一旦談到為什麼會產生這樣的結果時會覺得難以回答,說到底,還是對底層的原理未做深入的探究,本篇將從較為底層的原理層面來聊聊關於mysql的mvcc原理,瞭解並掌握了mvcc原理,也就能真正回答這些問題了。

一、mysql 資料寫入磁碟流程

在瞭解mvcc原理之前,先來看下面這種圖,這是一張關於使用者端發起一條update 資料的語句時,mysql 的innodb引擎所作的一些列操作過程(可按照前面的序列號);

從這張圖,我們提取如下關鍵資訊:

  • update 語句到達mysql的innodb引擎之後,並不是直接操作磁碟進行資料修改,而是先將磁碟資料load到buffer pool(如果沒有的話);
  • buffer poo中update完成之後,並不是立即刷到磁碟,還需要將資料寫到 undolog和redolog;
  • undolog記錄了資料修改前的記錄,redolog記錄的是事務提交時資料頁的物理修改;
  • 提交事務時,資料刷寫到磁碟,同時把所有修改資訊都存到該紀錄檔檔案(redolog), 用於在重新整理髒頁到磁碟,發生錯誤時, 進行資料恢復使用;
  • 資料確認落盤成功後,redolog就沒有作用了,innodb將會自動清理redolog;

從上面的分析中,可以看出,redolog檔案在整個執行過程中起到了非常重要的作用,有必要對該檔案做一些深入的瞭解和學習;

二、redo log

又叫重做紀錄檔,記錄的是事務提交時資料頁的物理修改,用來實現事務的永續性

redo log 紀錄檔檔案由兩部分組成:

  • 重做紀錄檔緩衝(redo log buffer),儲存在記憶體中,容易丟失,對應於mysql組態檔引數為:innodb_log_buffer_size,redo log buffer 大小,預設 16M ,最大值是4096M,最小值為1M,可以通過命令:show variables like '%innodb_log_buffer_size%' 進行檢視;
  • 以及重做紀錄檔檔案(redo logfile),儲存在磁碟中,是持久的;

1、redolog 的整體流程

仍然以上面流程圖中的更新一條資料的事務過程分析,來看redolog的整體流轉過程

具體步驟如下:

  • 將原始資料從磁碟中load到記憶體,修改資料的記憶體拷貝(buffer pool);
  • 生成一條重做紀錄檔,並寫入redo log buffer,記錄的是資料被修改後的值;
  • 事務commit時,將redo log buffer中的內容重新整理到 redo log file,對 redo log file採用追加
  • 寫的方式;
  • 定期將記憶體中修改的資料重新整理到磁碟中;

2、為什麼需要 redo log

在 InnoDB引擎中的記憶體結構中,主要記憶體區域就是緩衝池, 在緩衝池中快取了很多的數 據頁(磁碟中讀取mysql資料時一般以資料頁為單位進行載入); 在一個事務執行中,比如執行多個增刪改的操作時, InnoDB 引擎會先操作緩衝池中的資料,如果 緩衝區沒有對應的資料,再通過後臺執行緒將磁碟中資料load出來,放到緩衝區,然後修改緩衝池中 的資料,修改後的資料頁我們稱為髒頁; 而髒頁則會在一定的時機,通過後臺執行緒重新整理到磁碟中,從而保證緩衝區與磁碟的資料一致。 但是緩衝區髒頁資料並不是實時重新整理的,而是隔一段時間後才將緩衝區的資料刷到磁碟中。 假如重新整理到磁碟的過程出錯了,而提示給使用者事務提交成功,而資料卻 沒有持久化下來,這就出現問題了,沒有保證事務的永續性。 有了 redolog 之後,當對緩衝區的資料進行增刪改之後,會首先將操作的資料頁的變化,記錄在 redo log buffer中。在事務提交時,會將 redo log buffer 中的資料重新整理到 redo log 磁碟檔案中。 過一段時間後,如果重新整理緩衝區的髒頁到磁碟時,發生錯誤,此時就可以藉助於 redo log 進行資料 恢復,這樣就保證了事務的永續性。 而如果髒頁成功重新整理到磁碟 或 或者涉及到的資料已經落盤,此 時redolog 就沒有作用了,就可以刪除了,所以存在的兩個 redolog 檔案是迴圈寫的。 說到這裡就有夥伴要問,為什麼每一次提交事務,要重新整理 redo log 到磁碟中呢,而不是直接將 buffer pool 中的髒頁重新整理 到磁碟呢? 因為使用者端與mysql進行資料互動(IO)過程中,們運算元據一般都是隨機讀寫磁碟的(隨機讀寫比較慢),而不是順序讀寫磁碟(順序讀寫塊)。 而 redo log 在 往磁碟檔案中寫入資料,由於是紀錄檔檔案,所以都是順序寫的。順序寫的效率,要遠大於隨機寫。 這 種先寫紀錄檔的方式,也稱之為 WAL ( Write-Ahead Logging )。

三、undo log

undo log 也成為回滾紀錄檔,用於記錄資料被修改前的資訊 , 作用包含兩個 : 提供回滾 ( 保證事務的原子性 ) 和 MVCC(多版本並行控制 ) 。

舉例來說,本次使用update語句修改了一條id為1的資料,如果事務提交失敗,那麼就需要回滾資料,mysql引擎怎麼知道回滾到哪裡呢?那就要藉助undo log了,undolog中記錄了修改之前的資料,所以就可以用於事務回滾。

1、undo log 特點

  • undo log和redo log記錄物理紀錄檔不一樣,它是邏輯紀錄檔;
  • 當delete一條記錄時,undo log中會記錄一條對應的insert記錄,反之亦然,當update一條記錄時,它記錄一條對應相反的 update記錄;
  • 執行rollback時,就可以從undo log中的邏輯記錄讀取到相應的內容並進行回滾;

2、undo log 型別

  • insert undo log;
  • update undo log;

3、undo log 生成過程

從文章開頭的流程圖中再簡單抽象出下面的簡化執行步驟

在開啟一個事務對一條資料記錄進行update的時候,對於這條資料行來說,其底層儲存的結構大概長下面這樣;

 在這行記錄中,對應著兩個隱藏欄位,事務ID和回滾指標,當執行一條insert語句時,

begin ;
insert into user (name) values ( "tom" );

對於 undolog 來說,記錄的資料狀態將會呈現如下效果,可以看到,在這條記錄中,回滾指標指向了一條資料激勵,記錄了這條資料的源資訊,通過一個undo no標識;

執行update的時候,資料行記錄變更,同時在redo log 回滾指標鏈上將增加一條記錄,並連線上一條記錄;

 繼續執行一個update語句:

UPDATE user SET name ='jike'   WHERE id= 1 ;

4、undo log 回滾過程

如果事務回滾,執行rollback,對應的流程如下:

  • 通過undo no=3的紀錄檔把name='jike'的資料刪除;
  • 通過undo no=2的紀錄檔把id=1的資料的deletemark還原成0;
  • 通過undo no=1的紀錄檔把id=1的資料的name還原成Tom;
  • 通過undo no=0的紀錄檔把id=1的資料刪除;

5、undo log的刪除

undo log的刪除分成2種

  • 針對於insert undo log,因為insert操作的記錄,只對事務本身可見,對其他事務不可見。故該undo log可以在事務提交後直接刪除,不需要進行purge操作;
  • 針對於update undo log,該undo log可能需要提供MVCC機制,因此不能在事務提交時就進行刪除。提交時放入undo log連結串列,等待purge執行緒進行最後的刪除;

四、mvcc

1、什麼是MVCC

全稱:多版本並行控制,MVCC 是通過資料行的多個版本管理來實現資料庫的並行控制。通過這項技術,使得在InnoDB的事務隔離級別下執行 一致性讀操作有了保證。換言之,就是為了查詢一些正在被另一個事務更新的資料行,並且可以看到它們被更新之前的值,這樣在做查詢的時候就不用等待另一個事務釋放鎖。

2、MVCC組成

mvcc的實現主要依賴下面的3個主要邏輯實現,分別是:

  • 隱藏欄位,在上文中有所交待,每個資料行都會存在一個隱藏欄位;
  • undolog版本鏈,上文有所交待,記錄了回滾資料行的資料;
  • ReadView(讀檢視)是快照讀SQL執行時MVCC提取資料的依據,記錄並維護系統當前活躍的事務(未提交的)id,可能是一個陣列;

3、快照讀與當前讀

MVCC在MySQL InnoDB中的實現主要是為了提高資料庫並行效能,用更好的方式去處理讀-寫衝突,這樣即使有讀寫衝突時,也能做到不加鎖,非阻塞並行讀 ,而這個讀指的就是快照讀,而非當前讀。當前讀實際上是一種加鎖的操作,是悲觀鎖的實現,而MVCC本質是採用樂觀鎖思想的一種方式。

快照讀

快照讀又叫一致性讀,讀取的是快照資料。不加鎖的簡單的 SELECT 都屬於快照讀,即不加鎖的非阻塞讀;比如這樣:

SELECT * FROM player WHERE ...

之所以出現快照讀的情況,是基於提高並行效能的考慮,快照讀的實現是基於MVCC,它在很多情況下,避免了加鎖操作,降低了開銷。

既然是基於多版本,那麼快照讀可能讀到的並不一定是資料的最新版本,而有可能是之前的歷史版本。快照讀的前提是隔離級別不是序列級別,序列級別下的快照讀會退化成當前讀。

當前讀

當前讀讀取的是記錄的最新版本(最新資料,而不是歷史版本的資料),讀取時還要保證其他並行事務不能修改當前記錄,會對讀取的記錄進行加鎖。加鎖的 SELECT,或者對資料進行增刪改都會進行當前讀。比如:

SELECT * FROM student LOCK IN SHARE MODE; # 共用鎖

SELECT * FROM student FOR UPDATE; # 排他鎖

INSERT INTO student values ... # 排他鎖

DELETE FROM student WHERE ... # 排他鎖

五、mvcc操作演示

來看下面這一些列的事務操作過程,如下是一組操作同一條資料的記錄的多個事務,從事務2 ~ 事務5,分別對應不同的操作何階段;

從上文我們對undolog的瞭解,每次修改一條資料時,會在undolog 回滾鏈中增加一條記錄,用於後續做資料回滾;

具體步驟如下:

比如當事務2執行第一條修改語句時,會記錄一條undo log紀錄檔,記錄了當前資料變更之前的樣子; 然後更新記錄,並且記錄本次操作的事務ID,回滾指標,回滾指標用來指定如果發生回滾,回滾到哪一個版本;

當事務 3 執行第一條修改語句時,也會記錄 undo log 紀錄檔,記錄資料變更之前的樣子 ; 然後更新記 錄,並且記錄本次操作的事務 ID ,回滾指標,回滾指標用來指定如果發生回滾,回滾到哪一個版本;

當事務 4 執行第一條修改語句時,也會記錄 undo log 紀錄檔,記錄資料變更之前的樣子 ; 然後更新記 錄,並且記錄本次操作的事務 ID ,回滾指標,回滾指標用來指定如果發生回滾,回滾到哪一個版本;

通過上面一些列的操作,最終會發現,不同事務或相同事務對同一條記錄進行修改,會導致該記錄的 undolog 生成一條 記錄版本連結串列,連結串列的頭部是最新的舊記錄,連結串列尾部是最早的舊記錄。

 有了上面的redo log 的回鏈,最終是怎麼確定某個事務讀取的資料是長什麼樣子呢?接下來 readview就派上用場了;

ReadView (讀檢視)是 快照讀 SQL 執行時 MVCC 提取資料的依據,記錄並維護系統當前活躍的事務 (未提交的)id;

ReadView中包含了四個核心欄位:

欄位
含義
m_ids
當前活躍事務 ID 集合
min_trx_id
最小活躍事務 ID
max_trx_id
預分配事務 ID ,當前最大事務 ID+1 (因為事務 ID 是自增的)
creator_trx_id
ReadView 建立者事務 ID

而在 readview 中就規定了版本鏈資料的存取規則,

trx_id 代表當前undolog版本鏈對應事務ID

完整的匹配規則如下:

條件
是否可以存取
說明
trx_id ==
creator_trx_id
可以存取該版本
成立,說明資料是當前這個事
務更改的
trx_id < min_trx_id
可以存取該版本
成立,說明資料已經提交了
trx_id > max_trx_id
不可以存取該版本
成立,說明該事務是在
ReadView 生成後才開啟
min_trx_id <= trx_id
<= max_trx_id
如果 trx_id 不在 m_ids 中,
是可以存取該版本的
成立,說明資料已經提交

不同的隔離級別,生成ReadView的時機不同:

  • READ COMMITTED (讀已提交):在事務中每一次執行快照讀時生成ReadView;
  • REPEATABLE READ(可重複讀):僅在事務中第一次執行快照讀時生成ReadView,後續複用該ReadView;

也就是說,在利用mvcc的多版本並行控制時,只需要關注這兩種事務隔離級別就行了,接下來,以上午的excel中展示的幾個事務為例,對照這兩種型別的事務隔離級別進行說明;

1、READ COMMITTED 隔離級別

以事務5為例進行說明,兩次快照讀讀取資料時,是如何獲取資料的?

在事務 5 中,查詢了兩次 id 為 30 的記錄,由於隔離級別為 Read Committed ,所以每一次進行快照讀 都會生成一個 ReadView ,那麼兩次生成的 ReadView 如下

那麼這兩次快照讀在獲取資料時,就需根據所生成的 ReadView 以及 ReadView 的版本鏈存取規則, 到undolog 版本鏈中匹配資料,最終決定此次快照讀返回的資料; 先來看第一次快照讀具體的讀取過程

對應的undo log 版本鏈如下

在進行匹配時,會從undo log的版本鏈,從上到下進行挨個匹配:

1)先匹配下面這條記錄,這條記錄對應的trx_id為4,也就是將4帶入右側的匹配規則中。 ①不滿足 ②不滿足 ③不滿足 ④也不滿足 , 都不滿足,則繼續匹配undo log版本鏈的下一條;

2) 再匹配第二條記錄, 這條 記錄對應的 trx_id 為 3 ,也就是將 3 帶入右側的匹配規則中。①不滿足 ②不滿足 ③不滿足 ④也 不滿足 ,都不滿足,則繼續匹配 undo log 版本鏈的下一條;

3)再匹配第三條記錄,這條記 錄對應的trx_id為2,也就是將2帶入右側的匹配規則中。①不滿足 ②滿足 終止匹配,此次快照 讀,返回的資料就是版本鏈中記錄的這條資料;

再來看第二次快照讀具體的讀取過程:

 對應的undolog 版本鏈如下:

在進行匹配時,會從 undo log 的版本鏈,從上到下進行挨個匹配:

1)先匹配這條記錄,這條記錄對應的 trx_id為4,也就是將4帶入右側的匹配規則中。 ①不滿足 ②不滿足 ③不滿足 ④也不滿足 , 都不滿足,則繼續匹配undo log版本鏈的下一條;

2)再匹配第二條, 這條 記錄對應的 trx_id 為 3 ,也就是將 3 帶入右側的匹配規則中。①不滿足 ②滿足 。終止匹配,此次 快照讀,返回的資料就是版本鏈中記錄的這條資料;

2、REPEATABLE READ 隔離級別

在這種隔離級別下,僅在事務中第一次執行快照讀時生成 ReadView ,後續繼續複用該 ReadView 。 而 可重複讀在一個事務中,執行兩次相同的select 語句,查詢到的結果是一樣的; 那 MySQL 是如何做到可重複讀的呢 ? 按照上面的過程做一下類似的分析

可以看到,在可重複讀這種事務 隔離級別下,只在事務中第一次快照讀時生成 ReadView ,後續都複用該 ReadView。既然 ReadView 都一樣, ReadView 版本鏈匹配規則也一樣, 最終快照讀返 回的結果也是一樣的了。 總結 MVCC實現原理是通過 InnoDB表的隱藏欄位、UndoLog 版本鏈、ReadView來實現的;MVCC + 鎖,則實現了事務的隔離性;一致性則是由redolog 與 undolog保證;

到此這篇關於mysql mvcc 原理詳解的文章就介紹到這了,更多相關mysql mvcc 原理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com