在 MySQL 中是如何經過 MVCC 機制來解決不可重複讀和幻讀問題的?

前言

接上篇文章《一文搞懂 undo log 版本鏈與 ReadView 機制如何讓事務讀取到該讀的數據》,本文接下來介紹在可重複讀隔離級別下,MySQL 是如何解決不可重複讀和幻讀問題的?mysql

本文的內容嚴重依賴上篇文章的知識,建議讀者先閱讀上篇文章。web

不可重複讀

「不可重複讀現象指的是,在一個事務內,連續兩次查詢同一條數據,查到的結果先後不同」sql

在 MySQL 的可重複讀隔離級別下,不存在不可重複讀的問題,那麼 MySQL 是如何解決的呢?數據庫

答案就是 MVCC 機制。MVCC 是 Mutil-Version Concurrent Control(多版本併發控制)的縮寫,它指的是數據庫中的每一條數據,會存在多個版本。對同一條數據而言,MySQL 會經過必定的手段(ReadView 機制)控制每個事務看到不一樣版本的數據,這樣也就解決了不可重複讀的問題。數組

假設現有一條數據,它的 row_trx_id=10,數據的值爲 data0,它的 roll_pointer 指針爲 null。微信

假設如今有事務 A 和事務 B 併發執行,事務 A 的事務 id 爲 20,事務 B 的事務 id 爲 30。併發

如今事務 A 開始第一次查詢數據,那麼此時 MySQL 會爲事務 A 產生一個 ReadView,此時 ReadView 的內容以下:m_ids=[20,30],min_trx_id=20,max_trx_id=31,creator_trx_id=20。編輯器

此時因爲數據的最新版本的 row_trx_id=10,「小於事務 A 的 ReadView 中的 min_trx_id,這代表這個版本的數據是在事務 A 開啓以前就提交的」,所以事務 A 能夠讀取到數據,讀取到的值爲 data0。flex

「結論:事務 A 第一次查詢到的數據爲 data0」url

接着事務 B(trx_id=30)去修改數據,將數據修改成 data_B,並提交事務,此時 MySQL 會寫一條對應的 undo log,數據就會新增一個版本,undo log 版本就變成了以下圖所示的結構,數據的最新版本的 row_trx_id 就是事務 B 的事務 id,即:30。

此時,事務 B 已經提交了,所以系統中活躍事務的數組裏就沒有 30 這個 id 了。

「重點來了,事務 A 的 ReadView 是在發起第一次查詢的時候建立的,當時系統中的活躍事務有 20 和 30 這兩個 id,那麼此時當事務 B 提交之後,事務 A 的 ReadView 的 m_ids 會變化嗎?不會。由於是可重複讀隔離級別下,對於讀事務,只會在事務查詢的第一次建立 ReadView,後面的查詢不會再從新建立」

接着事務 A(trx_id=20)開始第二次查詢數據,前面事務 A 已經建立了 ReadView,因此在第二次查詢時,不會再重複建立 ReadView 了。

此時在 undo log 版本鏈中,數據最新版本的事務 id 爲 30,根據 ReadView 機制(什麼是 ReadView 機制,能夠去閱讀上一篇文章),發現 30 處於事務 A 的 ReadView 中 min_trx_id 和 max_trx_id 之間,所以還須要判斷 30 是否處於 m_ids 數組內,結果發現 30 確實在 m_ids 數組中,「這就表示這個版本的數據是和本身在同一時刻開啓事務所提交的,所以不能讓本身讀取。」

因此此時事務 A 須要沿着 undo log 版本鏈繼續向前找,最終發現 row_id=10 的版本數據本身能夠讀取到,所以事務 A 查詢到的值是 data0。

「結論:事務 A 第二次查詢到的數據爲 data0。這與事務 A 第一次查詢的數據結果相同,沒有出現不可重複讀的現象。」

那假設後來又建立了一個事務 C,id 爲 40,而且事務 C 將數據修改成了 data_C。而後數據的 undo log 版本鏈變爲了以下如所示。

而後事務 A 發起第三次查詢,此時事務 A 仍然不會再從新建立 ReadView,因此此時它的 ReadView 依舊是:m_ids=[20,30],min_trx_id=20,max_trx_id=31,creator_trx_id=20。

因爲數據最新的版本的爲 trx_id=40,依照 ReadView 機制,40 大於事務 A 中的 max_trx_id,「這表示這是在事務 A 開啓以後的事務提交的數據,所以事務 A 不能讀取到」,因此須要沿着 undo log 版本鏈往前找,然而 trx_id=30 的版本事務 A 也不能讀到,繼續向前找,最終讀取到 trx_id=10 的版本數據,即 data0。

這樣,在事務 A 內,一共發起了 3 次查詢,每次查詢的數據都是 data0,沒有出現不可重複讀的現象。

幻讀

幻讀特指後面的查詢比前面的查詢的記錄條數多,看到了前面沒看到的數據,就像產生幻覺同樣,所以稱之爲幻讀。

快照讀與當前讀

在解釋 MySQL 的可重複讀隔離級別解決了幻讀問題以前,咱們先來看兩個定義:「快照讀與當前讀」

咱們知道,在事務開啓的時候,會基於當前系統中數據庫的數據,爲每一個事務生成一個快照,也叫作 ReadView,後面這個事務全部的讀操做都是基於這個 ReadView 來讀取數據,這種讀稱之爲快照讀。「咱們在實際的工做中,所使用的 SQL 查詢語句基本都是快照讀。」

經過前面介紹的 undo log 版本鏈,咱們知道,每行數據可能會有多個版本,若是每次讀取時,「咱們都強制性的讀取最新版本的數據,這種讀稱之爲當前讀,也就是讀取最新的數據」。什麼樣的 SQL 查詢語句叫作當前讀呢?例如在 select 語句後面加上「for update 或者 lock in share mode」等。

# 加上排他鎖
select * from t for update;
# 加上共享鎖
select * from t for lock in share mode;

能夠發現,當前讀的這兩種寫法,在查詢過程當中都是須要加鎖的,所以它們能讀取到最新的數據。

「須要說明的是,在 MySQL 可重複讀隔離級別下,幻讀問題確實不存在。可是 MVCC 機制解決的是快照讀的幻讀問題,並不能解決當前讀的幻讀問題。當前讀的幻讀問題是經過間隙鎖解決的,至於什麼是間隙鎖,之後的文章中會介紹,有興趣的讀者能夠本身去了解。」

所以,「本文的後半部分,所有是基於快照讀來進行解釋的」

如何解決幻讀

假設如今表 t 中只有一條數據,數據內容中,主鍵 id=1,隱藏的 trx_id=10,它的 redo log 以下圖所示。

假設如今有事務 A 和事務 B 併發執行,事務 A 的事務 id 爲 20,事務 B 的事務 id 爲 30。

如今事務 A 開始第一次查詢數據,查詢的 SQL 語句以下。

select * from where id >= 1;

在開始查詢以前,MySQL 會爲事務 A 產生一個 ReadView,此時 ReadView 的內容以下:m_ids=[20,30],min_trx_id=20,max_trx_id=31,creator_trx_id=20。

因爲此時表 t 中只有一條數據,且符合 where id>=1 條件,所以會查詢出來。「而後經過 ReadView 機制,發現該行數據的 row_id=10,小於事務 A 的 ReadView 裏 min_trx_id,這表示這條數據是事務 A 開啓以前,其餘事務就已經提交了的數據,所以事務 A 能夠讀取到。」

「結論:事務 A 的第一次查詢,能讀取到一條數據,id=1。」

接着事務 B(trx_id=30),往表 t 中新插入兩條數據,SQL 語句以下。

insert into t(id,namevalues(2,'小明');
insert into t(id,namevalues(3,'小紅');

而後事務提交事務,那麼此時表 t 中就有三條數據了,對應的 undo 以下圖所示:

接着事務 A 開啓第二次查詢,根據可重複讀隔離級別的規則,此時事務 A 並不會再從新生成 ReadView。此時表 t 中的 3 條數據都知足 where id>=1 的條件,所以會先查出來,而後再根據 ReadView 機制,判斷每條數據是否是均可以被事務 A 看到。

  1. 首先 id=1 的這條數據,前面已經說過了,能夠被事務 A 看到。
  2. 而後是 id=2 的數據,它的 trx_id=30,此時事務 A 發現,這個值處於 min_trx_id 和 max_trx_id 之間,所以還須要再判斷 30 是否處於 m_ids 數組內。因爲事務 A 的 m_ids=[20,30],所以在數組內,這表示 id=2 的這條數據是與事務 A 在同一時刻啓動的其餘事務提交的,因此這條數據不能讓事務 A 看到。
  3. 同理,id=3 的這條數據,trx_id 也爲 30,所以也不能被事務 A 看見。

「結論:最終事務 A 的第二次查詢,只能查詢出 id=1 的這條數據。這和事務 A 的第一次查詢的結果是同樣的,所以沒有出現幻讀現象,因此說在 MySQL 的可重複讀隔離級別下,不存在幻讀問題。」

總結

本文結合 ReadView 機制,介紹了 MySQL 在可重複讀隔離級別下,是如何解決不可重複讀和幻讀問題的,其核心點在於 ReadView 的原理,以及在可重複讀隔離級別下,若是事務只是進行查詢操做,那麼就「只會在第一次查詢的時候生成 ReadView 快照,這一點和讀提交隔離級別是最大的區別」

同時,文中還簡單介紹了快照讀和當前讀的區別,快照讀指的是基於 ReadView 讀取數據,當前讀指的是讀取數據的最新版本。

另外,須要注意的是,文中只是介紹了 MVCC 如何解決快照讀的幻讀問題,而當前讀的幻讀問題,則是經過間隙鎖來解決的。


本文分享自微信公衆號 - MySQL解決方案工程師(mysqlse)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索