InnoDB爲什麼那麼快?秒級快照原理與當前讀!

我敢百分百這麼說,這道題沒有個人會的!

Mysql的搜索引擎我們大家都知道,MyISAM,InnoDB和memory,那麼InnoDB爲什麼會在事務在啓動的時候就”拍了個快照”,這個一般很少有人在知道。

今天我們聊一下InnoDB秒級快照原理

在之前爲文章《分析事務隔離的實現》中我們提到:如果是可重複讀隔離級別,事務T啓動的時候會創建一個視圖 read-view,之後事務 T 執行期間,即使有其他事務修改了數據,事務 T 看到的仍然跟在啓動時看到的一樣。也就是說,一個在可重複讀隔離級別下執行的事務,好像與世無爭,不受外界影響。但是,我在介紹MySQL鎖機制的文章中《探究MySQL鎖機制》 的時候,一個事務要更新一行,如果剛好有另外一個事務擁有這一行的行鎖,它會被鎖住,進入等待狀態。問題是,既然進入了等待狀態,那麼等到這個事務自己獲取到行鎖要更新數據的時候,它讀到的值又是什麼呢?

開始事務的兩種方式

需要注意的是:begin/start transaction 命令並不是一個事務的起點,在執行到它們之後的第一個操作InnoDB 表的語句,事務才真正啓動。 如果你想要馬上啓動一個事務,可以使用transaction with consistent snapshot 這個命令!

第一種啓動方式,一致性視圖是在第執行第一個快照讀語句時創建的;

第二種啓動方式,一致性視圖是在執行 start transaction with consistent snapshot 時創建的;

事務 C 沒有顯式地使用 begin/commit,表示這個 update 語句本身就是一個事務,語句完成的時候會自動提交。事務 B 在更新了行之後查詢 ; 事務 A 在一個只讀事務中查詢,並且時間順序上是在事務 B 的查詢之後。但是結果卻是:事務 B 查到的 k 的值是 3,而事務 A 查到的 k 的值是 1

按照我們想象的情況應該是如下所示:

最終得到的結果是事務A查詢的結果爲1,事務B查詢的結果是2,但是爲什麼事務B查詢後爲3呢??

需要注意的是,在 MySQL 裏,有兩個視圖的概念:

1、一個是view,它是一個用查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果。創建視圖的語法是 create view … ,而它的查詢方法與表一樣。

2、另一個是 InnoDB 在實現 MVCC(Multi-Version Concurrency Control,多版本併發控制)時用到的一致性讀視圖,即 consistent read view,用於支持 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別的實現。

InnoDB創建秒級快照原理

本文的首圖即是MVCC的實現原理,在可重複讀隔離級別下,事務在啓動的時候就”拍了個快照”。注意,這個快照是基於整庫的。如果一個庫有 100G,那麼我啓動一個事務,MySQL就要拷貝 100G 的數據出來,這個過程得多慢啊。可是平時的事務執行起來很快啊,實際上,我們並不需要拷貝出這 100G 的數據。我們先來看看這個快照是怎麼實現的:

InnoDB 裏面每個事務有一個唯一的事務 ID,叫作 transaction id。它是在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。而每行數據也都是有多個版本的。每次事務更新數據的時候,都會生成一個新的數據版本,並且把 transaction id 賦值給這個數據版本的事務 ID,記爲 row trx_id。同時,舊的數據版本要保留,並且在新的數據版本中,能夠有信息可以直接拿到它。也就是說,數據表中的一行記錄,其實可能有多個版本 (即row中的每個數據),每個版本有自己的 row trx_id。

如所示,就是一個記錄被多個事務連續更新後的狀態。

圖中的三個虛線箭頭,就是 undo log(回滾日誌);而 V1、V2、V3 並不是物理上真實存在的,而是每次需要的時候根據當前版本和 undo log 計算出來的。比如,需要 V2 的時候,就是通過 V4 依次執行 U3、U2 算出來。

按照可重複讀的定義,一個事務啓動的時候,能夠看到所有已經提交的事務結果。但是之後,這個事務執行期間,其他事務的更新對它不可見。

因此,一個事務只需要在啓動的時候聲明說,“以我啓動的時刻爲準,如果一個數據版本是在我啓動之前生成的,就認;如果是我啓動以後才生成的,我就不認,我必須要找到它的上一個版本”。如果是這個事務自己更新的數據,它自己還是要認的。

在實現上, InnoDB 爲每個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正處於啓動了但還沒提交的所有事務ID。數組裏面事務 ID 的最小值記爲低水位,當前系統裏面已經創建過的事務 ID 的最大值加1記爲高水位。這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)而數據版本的可見性規則,就是基於數據的 row trx_id 和這個一致性視圖的對比結果得到的。這個視圖數組把所有的 row trx_id 分成了幾種不同的情況:

這樣,對於當前事務的啓動瞬間來說,一個數據版本的 row trx_id,有以下幾種可能:

1、如果落在綠色部分,表示這個版本是已提交的事務或者是當前事務自己生成的,這個數據是可見的;

2、如果落在紅色部分,表示這個版本是由將來啓動的事務生成的,是肯定不可見的;

3、如果落在黃色部分,那就包括兩種情況

  • 若 row trx_id 在數組中,表示這個版本是由還沒提交的事務生成的,不可見;
  • 若 row trx_id 不在數組中,表示這個版本是已經提交了的事務生成的,可見。

重點要理解落在黃色部分的兩種情況,一條數據被多個事物更新(事務還未提交),那麼肯定在數組中含有row trx_id,即這個版本是由還沒提交的事務生成的

所以InnoDB如何秒級創建快照的呢?我的總結如下:

1、事務開啓時InnoDB會賦給事務一個ID,爲了方便我們直接叫做事務ID

2、表中每行數據有多個版本,A事務更新數據的時候,都會生成一個新的數據版本,並且把A事務的事務ID賦值給這個數據版本的事務ID

3、通過回滾日誌來計算事務ID對應的數據版本,比如A事務更新了 id=1的數據的value爲1,那麼就存在id=1那一條數據的新版本,(id=1,value=1)這一條數據對應的事務ID就是A事務的事務ID

4、如果一個數據版本的事務ID落在黃色部分並且還在事務數組中就判定此數據版本是其他未提交事務對數據修改產生的數據新版本;如果一個數據版本的事務ID落在黃色部分並且不在自己的事務數組中,那麼說明事務已經提交,是合法數據

所以InnoDB的秒級快照的創建能力的原理無非和Linux的AUFS文件系統如出一撤,那就是你只要改了某一條數據,我就複製那一條讓你改,並且不會影響其他人的數據:

都是”當前讀”惹的禍

我們在明白了InnoDB如何實現的創建秒級快照的原理後,開篇的疑惑其實很好解答:

我們不妨做如下假設,事務 A 開始前,系統裏面只有一個活躍事務 ID 是 99;事務 A、B、C 的版本號分別是 100、101、102,且當前系統裏只有這四個事務;三個事務開始前,(1,1)這一行數據的 row trx_id 是 90

這樣,事務 A 的視圖數組就是 [99,100],事務 B 的視圖數組是 [99,100,101],事務 C 的視圖數組是 [99,100,101,102]。

這其實就很容易理解了,事務C發現,此時的已開啓事務但是未提交的有四個(99,100,101,102),由於其他事務還並未對id=1這條數據進行修改,所以此時只有一個版本那就是(id=1, k=1,事務ID=90),並且90號事務ID處於低水位,說明事務ID爲90的事務已經提交了,數據有效,隨後事務C把k置爲了2,並且提交了事務,所以此時生成了新版本(id=1,k=2,事務ID爲102)

最後A事務去查詢k的值,發現已經有三個版本了,當A事務看到最新版本(id=1,k=3,事務ID爲101)的數據時,發現101在高水位,不能讀到,接着讀到版本爲(id=1,k=2,事務ID爲102)時候同樣事務102也是高水位,不能讀到,繼續讀,讀到(id=1,k=1,事務ID爲90)的版本的數據,事務ID爲90的事務處於低水位,數據時可見的,於是得到的數據依舊是(id=1, k=1, 事務ID爲90)的數據版本

問題來了?事務B是如何看到事務C的修改結果的??對於事務B來說,事務C不是高水位嗎?按道理應該不可見高水位事務的修改呀!!!

事務 B 的視圖數組是先生成的,之後事務 C 才提交,不是應該看不見 (1,2)嗎,怎麼能算出 (1,3) 來?

是的,如果事務 B 在更新之前查詢一次數據,這個查詢返回的 k 的值確實是 1。但是,當它要去更新數據的時候,就不能再在歷史版本上更新了,否則事務 C 的更新就丟失了。因此,事務 B 此時的 set k=k+1 是在(1,2)的基礎上進行的操作。所以,這裏就用到了這樣一條規則:

更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲“當前讀”(current read)。

因此,在更新的時候,當前讀拿到的數據是 (1,2),更新後生成了新版本的數據 (1,3),這個新版本的 row trx_id 是 101。所以,在執行事務 B 查詢語句的時候,一看自己的版本號是 101,最新數據的版本號也是101,是自己的更新,可以直接使用,所以查詢得到的 k 的值是 3。

但是select的結果卻是1,所以我們隊開始的例子稍做修改,讓更新的時候值不在是k=k+1,這樣就避免了當前讀,所以下面的例子就是避免的當前讀所產生的結果:

因爲在MYSQL中:不能在同一表中查詢的數據作爲同一表的更新數據,所以我這裏使用了臨時表

其實,除了 update 語句外,select 語句如果加鎖,也是當前讀。

現在假設事務 C 不是馬上提交的,而是變成了下面的事務 C’,會怎麼樣呢?

事務 C’的不同是,更新後並沒有馬上提交,在它提交前,事務 B 的更新語句先發起了。前面說過了,雖然事務 C’還沒提交,但是 (1,2) 這個版本也已經生成了,並且是當前的最新版本。那麼,事務 B 的更新語句會怎麼處理呢?。

事務 C’沒提交,也就是說 (1,2) 這個版本上的寫鎖還沒釋放。而事務 B 是當前讀,必須要讀最新版本,而且必須加鎖,因此就被鎖住了,必須等到事務 C’ 釋放這個鎖,才能繼續它的當前讀。

到這裏,我們把一致性讀、當前讀和行鎖就串起來了。

可重複讀的實現

可重複讀的核心就是一致性讀(consistent read);而事務更新數據的時候,只能用當前讀。如果當前的記錄的行鎖被其他事務佔用的話,就需要進入鎖等待。

而讀提交的邏輯和可重複讀的邏輯類似,它們最主要的區別是:在可重複讀隔離級別下,只需要在事務開始的時候創建一致性視圖,之後事務裏的其他查詢都共用這個一致性視圖;在讀提交隔離級別下,每一個語句執行前都會重新算出一個新的視圖。

InnoDB 的行數據有多個版本,每個數據版本有自己的 row trx_id,每個事務或者語句有自己的一致性視圖。普通查詢語句是一致性讀,一致性讀會根據 row trx_id 和一致性視圖確定數據版本的可見性。對於可重複讀,查詢只承認在事務啓動前就已經提交完成的數據;對於讀提交,查詢只承認在語句啓動前就已經提交完成的數據;而當前讀,總是讀取已經提交完成的最新版本。

作者:zchanglin
鏈接:https://juejin.cn/post/6951281035401232415
來源:掘金

關於如何學習Java,一方面需要不斷的去學習,把基礎知識學紮實,另一方面也要認識到java的學習不能僅僅靠理論,更多的是靠實操,所以要多練習多做項目,在實踐中學習纔是最好的學習方法。很多人剛開始不知道怎麼去學習,這裏我和大部分都來自好朋友整理的一份《JavaCodeHub面試突擊》,裏面包含的內容實在是太全面了,真的很能考察出一名應聘者的成色。

JavaCodeHub

我是終端研發部的小於哥 面試過很多很多應聘者,說實話, 現在面試要求可真高,雖然工作擰螺絲,但面試還是造火箭的。很多東西我們不光要會用,也要懂其原理。在戰術上一定要重視,方能百戰不殆!如果有幫助,歡迎點贊!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章