前文我們介紹了 InnoDB 儲存引擎在事務隔離級別 READ COMMITTED 和 REPEATABLE READ(預設)下會開啟一致性非鎖定讀,簡單回顧下:所謂一致性非鎖定讀就是每行記錄可能存在多個歷史版本,多版本之間串聯起來形成了一條版本鏈,這樣不同時刻啟動的事務可以無鎖地訪問到不同版本的資料。

undo log 版本鏈

一致性非鎖定讀是通過 MVCC(Multi Version Concurrency Control,多版本併發控制) 來實現的。事實上,MVCC 沒有一個統一的實現標準,所以各個儲存引擎的實現機制不盡相同。

InnoDB 儲存引擎中 MVCC 的實現是通過 undo log 來完成的,undo log 是啥?

簡單理解,undo log 就是每次操作的反向操作,比如比如當前事務執行了一個插入 id = 100 的記錄的操作,那麼 undo log 中儲存的就是刪除 id = 100 的記錄的操作。

所以,這裡用多版本來形容並不是非常準確,因為 InnoDB 並不會真正地去開闢空間儲存多個版本的行記錄,只是藉助 undo log 記錄每次寫操作的反向操作。

也就是說,B+ 索引樹上對應的記錄只會有一個最新版本,只不過 InnoDB 可以根據 undo log 得到資料的歷史版本,從而實現多版本控制。

那麼,還有個問題,undo log 是如何和某條行記錄產生聯絡的呢?換句話說,我怎麼能通過這條行記錄找到它擁有的 undo log 呢?

具體來說,InnoDB 儲存引擎中每條行記錄其實都擁有兩個隱藏的欄位:trx_idroll_pointer

從名字也能看出來,trx_id 就是最近更新這條行記錄的事務 ID,roll_pointer 就是指向之前生成的 undo log。

掏出我們的 user 表,來舉個例子,假設 id = 100 的事務 A 插入一條行記錄(id = 1, username = "Jack", age = 18),那麼,這行記錄的兩個隱藏欄位 trx_id = 100roll_pointer 指向一個空的 undo log,因為在這之前並沒有事務操作 id = 1 的這行記錄。如圖所示:

然後,id = 200 的事務 B 修改了這條行記錄,把 age 從 18 修改成了 20,於是,這條行記錄的 trx_id 就變成了 200,rooll_pointer 就指向事務 A 生成的 undo log :

接著,id = 300 的事務 C 再次修改了這條行記錄,把 age 從 20 修改成了 30,如下圖:

可以看到,每次修改行記錄都會更新 trx_id 和 roll_pointer 這兩個隱藏欄位,之前的多個數據快照對應的 undo log 會通過 roll_pointer 指標串聯起來,從而形成一個版本鏈

需要注意的是,select 查詢操作不會生成 undo log!在 InnoDB 儲存引擎中,undo log 只分為兩種:

  • insert undo log:在 insert 操作中產生的 undo log
  • update undo log:對 delete 和 update 操作產生的 undo log

事實上,由於事務隔離性的要求,insert 操作的記錄,只對事務本身可見,對其他事務不可見,對吧,所以也就不存在併發情況下的問題。所以,也就是說,MVCC 這個機制,其實就是靠 update undo log 實現的,和 insert undo log 基本上沒啥關係,我們上面說的 undo log 版本鏈上的其實就是 update undo log。

ReadView 機制

說到 MVCC,說到 undo log 版本鏈,如果你自己不往下說的話,八九不離十面試官都會問你下 ReadView 這個機制。

咱也不賣官子,直接說吧,ReadView 機制就是用來判斷當前事務能夠看見哪些版本的,一個 ReadView 主要包含如下幾個部分:

  • m_ids:生成 ReadView 時有哪些事務在執行但是還沒提交的(稱為 ”活躍事務“),這些活躍事務的 id 就存在這個欄位裡
  • min_trx_id:m_ids 裡最小的值
  • max_trx_id:生成 ReadView 時 InnoDB 將分配給下一個事務的 ID 的值(事務 ID 是遞增分配的,越後面申請的事務 ID 越大)
  • creator_trx_id:當前建立 ReadView 事務的 ID

接下來,再掏出 user 表,通過一個例子來理解下 ReaView 機制是如何做到判斷當前事務能夠看見哪些版本的:

假設表中已經被之前的事務 A(id = 100)插入了一條行記錄(id = 1, username = "Jack", age = 18),如圖所示:

接下來,有兩個事務 B(id = 200) 和 C(id = 300)過來併發執行,事務 B 想要更新(update)這行 id = 1 的記錄,而事務 C(select)想要查詢這行資料,這兩個事務都執行了相應的操作但是還沒有進行提交:

如果現在事務 B 開啟了一個 ReadView,在這個 ReadView 裡面:

  • m_ids 就包含了當前的活躍事務的 id,即事務 B 和事務 C 這兩個 id,200 和 300
  • min_trx_id 就是 200
  • max_trx_id 是下一個能夠分配的事務的 id,那就是 301
  • creator_trx_id 是當前建立 ReadView 事務 B 的 id 200

現在事務 B 進行第一次查詢(上面說過 select 操作不會生成 undo log 的哈),會把這行記錄的隱藏欄位 trx_id 和 ReadView 的 min_trx_id 進行下判斷,此時,發現 trx_id 是 100,小於 ReadView 裡的 min_trx_id(200),這說明在事務 B 開始之前,修改這行記錄的事務 A 已經提交了,所以開始於事務 A 提交之後的事務 B、是可以查到事務 A 對這行記錄的更新的

row.trx_id < ReadView.min_trx_id

接著事務 C 過來修改這行記錄,把 age = 18 改成了 age = 20,所以這行記錄的 trx_id 就變成了 300,同時 roll_pointer 指向了事務 C 修改之前生成的 undo log:

那這個時候事務 B 再次進行查詢操作,會發現這行記錄的 trx_id(300)大於 ReadView 的 min_trx_id(200),並且小於 max_trx_id(301)

row.trx_id > ReadView.min_trx_id && row.trx_id < max_trx_id

這說明一個問題,就是更新這行記錄的事務很有可能也存在於 ReadView 的 m_ids(活躍事務)中。所以事務 B 會去判斷下 ReadView 的 m_ids 裡面是否存在 trx_id = 300 的事務,顯然是存在的,這就表示這個 id = 300 的事務是跟自己(事務 B)在同一時間段併發執行的事務,也就說明這行 age = 20 的記錄事務 B 是不能查詢到的。

既然無法查詢,那該咋整?事務 B 這次的查詢操作能夠查到啥呢?

沒錯,undo log 版本鏈!

這時事務 B 就會順著這行記錄的 roll_pointer 指標往下找,就會找到最近的一條 trx_id = 100 的 undo log,而自己的 id 是 200,即說明這個 trx_id = 100 的 undo log 版本必然是在事務 B 開啟之前就已經提交的了。所以事務 B 的這次查詢操作讀到的就是這個版本的資料即 age = 18。

通過上述的例子,我們得出的結論是,通過 undo log 版本鏈和 ReadView 機制,可以保證一個事務不會讀到併發執行的另一個事務的更新

那自己修改的值,自己能不能讀到呢?

這當然是廢話,肯定可以讀到呀。不過上面的例子我們只涉及到了 ReadView 中的前三個欄位,而 creator_trx_id 就與自己讀自己的修改有關,所以這裡還是圖解出來讓大家更進一步理解下 ReadView 機制:

假設事務 C 的修改已經提交了,然後事務 B 更新了這行記錄,把 age = 20 改成了 age = 66,如下圖所示:

然後,事務 B 再來查詢這條記錄,發現 trx_id = 200 與 ReadView 裡的 creator_trx_id = 200 一樣,這就說明這是我自己剛剛修改的啊,當然可以被查詢到。

row.trx_id = ReadView.creator_trx_id

那如果在事務 B 的執行期間,突然開了一個 id = 400 的事務 D,然後更新了這行記錄的 age = 88 並且還提交了,然後事務 B 再去讀這行記錄,能讀到嗎?

答案是不能的。

因為這個時候事務 B 再去查詢這行記錄,就會發現 trx_id = 500 大於 ReadView 中的 max_trx_id = 301,這說明事務 B 執行期間,有另外一個事務更新了資料,所以不能查詢到另外一個事務的更新。

row.trx_id > ReadView.max_trx_id

那通過上述的例子,我們得出的結論是,通過 undo log 版本鏈和 ReadView 機制,可以保證一個事務只可以讀到該事務自己修改的資料或該事務開始之前的資料

小結

總結下,通過 undo log 版本鏈和 ReadView 機制:

  • 可以保證一個事務不會讀到併發執行的另一個事務的更新
  • 可以保證一個事務只可以讀到該事務自己修改的資料或該事務開始之前的資料

另外,前文說過,一致性非鎖定讀(或者直接說 MVCC 吧,畢竟一致性非鎖定讀也是靠 MVCC 實現的)只在事務隔離級別 READ COMMITTED 和 REPEATABLE READ(預設)下才會開啟,那對於這兩個隔離級別,其實最根本的不同之處,就在於它們生成 ReadView 的時機不同,這個我們留在下文解釋~

關注公眾號 | 飛天小牛肉,即時獲取更新

  • 博主東南大學碩士在讀,攜程 Java 後臺開發暑期實習生,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 技術棧等相關原創技術好文。關注公眾號第一時間獲取文章更新,後臺回覆 300 即可免費獲取極客大學出品的 Java 面試 300 題

  • 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.8k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~

  • 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 900+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。