MySQL是如何實現可重複讀的?

Photo by picography.coPhoto by picography.coweb

《MySQL實戰45講》筆記。數組

簡單理解一下可重複讀

可重複讀是指:一個事務執行過程當中看到的數據,老是跟這個事務在啓動時看到的數據是一致的。微信

咱們能夠簡單理解爲:在可重複讀隔離級別下,事務在啓動的時候就」拍了個快照「。注意,這個快照是基於整個庫的。編輯器

這時,你可能就會想,若是一個庫有 100G,那麼我啓動一個事務,MySQL就要拷貝 100G 的數據出來,這個過程得多慢啊。但是,我平時的事務執行起來很快啊。ui

實際上,咱們並不須要拷貝出這 100G 的數據。咱們來看下」快照「是怎麼實現的。url

拍個快照

InnoDB 裏面每一個事務都有一個惟一的事務 ID,叫做 transaction id。它在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。spa

每條記錄在更新的時候都會同時記錄一條 undo log,這條 log 就會記錄上當前事務的 transaction id,記爲 row trx_id。記錄上的最新值,經過回滾操做,均可以獲得前一個狀態的值。3d

以下圖所示,一行記錄被多個事務更新以後,最新值爲 k=22。假設事務A在 trx_id=15 這個事務提交後啓動,事務A 要讀取該行時,就經過 undo log,計算出該事務啓動瞬間該行的值爲 k=10。code

在可重複讀隔離級別下,一個事務在啓動時,InnoDB 會爲事務構造一個數組,用來保存這個事務啓動瞬間,當前正在」活躍「的全部事務ID。」活躍「指的是,啓動了但還沒提交。cdn

數組裏面事務 ID 爲最小值記爲低水位,當前系統裏面已經建立過的事務 ID 的最大值加 1 記爲高水位。

這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。

這個視圖數組把全部的 row trx_id 分紅了幾種不一樣的狀況。

  1. 若是 trx_id 小於低水位,表示這個版本在事務啓動前已經提交,可見;
  2. 若是 trx_id 大於高水爲,表示這個版本在事務啓動後生成,不可見;
  3. 若是 trx_id 大於低水位,小於高水位,分爲兩種狀況:
    1. 若 trx_id 在數組中,表示這個版本在事務啓動時還未提交,不可見;
    2. 若 trx_id 不在數組中,表示這個版本在事務啓動時已經提交,可見。

InnoDB 就是利用 undo log 和 trx_id 的配合,實現了事務啓動瞬間」秒級建立快照「的能力。

舉個栗子

初始化語句

CREATE TABLE `t` (
 `id` int(11) NOT NULL,  `k` int(11) DEFAULT NULL,  PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t(id, k) values(1,1),(2,2); 複製代碼

下表爲事務A, B, C 的執行流程

事務A 事務B 事務C
START TRANSACTION WITH CONSISTENT SNAPSHOT;
START TRANSACTION WITH CONSISTENT SNAPSHOT;
UPDATE t SET k=k+1 WHERE id=1;
UPDATE t SET k=k+1 WHERE id=1;
SELECT k FROM t WHERE id=1;
SELECT k FROM t WHERE id=1;
COMMIT;
COMMIT;

咱們假設事務A, B, C 的 trx_id 分別爲 100, 101, 102。事務A開始前活躍的事務 ID 只有 99,而且 id=1 這一行數據的 trx_id=90。

根據假設,咱們得出事務啓動瞬間的視圖數組:事務A:[99, 100],事務B:[99, 100, 101],事務C:[99, 100, 101, 102]。

  1. 事務C經過更新語句,把 k 更新爲 2,此時trx_id=102;
  2. 事務B經過更新語句,把 k 更新爲 3,此時trx_id=101;
  3. 事務B經過查詢語句,查詢到最新一條記錄爲3,trx_id=101,知足隔離條件,返回 k=3;
  4. 事務A經過查詢語句:
    1. 查詢到最新一條記錄爲3,trx_id=101,比高水位大,不可見;
    2. 經過 undo log,找到上一個歷史版本,trx_id=102,比高水位大,不可見;
    3. 繼續找上一個歷史版本,trx_id=90,比低水位小,可見。

提出問題:爲啥事務B更新的時候能看到事務C的修改?

咱們假設事務B在更新的看不到事務C的修改,是什麼個狀況?

  1. 事務B查詢到最新一條記錄爲2,trx_id=102,比高水位大,不可見;
  2. 經過 undo log,找到上一個版本,trx_id=90,比低水位小,可見;
  3. 返回記錄 k=1,執行 k=k+1,把 k 更新爲2,此時 trx_id=101。

若是是這種狀況,事務C可能就蒙了:「啥子狀況,個人更新怎麼就丟了」。事務B覆蓋了事務C的更新。

因此,InnoDB在更新時運用一條規則:更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲「當前讀「 (current read)。

所以,事務B在更新時要拿到最新的數據,在此基礎上作更新。緊接着,事務B在讀取的時候,查詢到最新的記錄爲3, trx_id=101 爲當前事務ID,可見。

咱們再假設另外一種狀況:

事務B在更新以後,事務C緊接着更新,事務B回滾了,事務C成功提交。

事務B 事務C
START TRANSACTION WITH CONSISTENT SNAPSHOT;
START TRANSACTION WITH CONSISTENT SNAPSHOT;
UPDATE t SET k=k+1 WHERE id=1;
UPDATE t SET k=k+1 WHERE id=1;
SELECT k FROM t WHERE id=1;
ROLLBACK;
COMMIT;

若是按照當前讀的定義,會發生如下事故,假設當前 K=1:

  1. 事務B把 k 更新爲 2;
  2. 事務C讀取到當前最新值,k=2,更新爲3;
  3. 事務B回滾;
  4. 事務C提交。

這時候,事務C發現本身想要執行的是 +1 操做,結果變成了 」+2「 操做。

InnoDB 確定不容許這種狀況的發生,事務B在執行更新語句時,會給該行加上行鎖,直到事務B結束,纔會釋放這個鎖。

小結

  1. InnoDB 的行數據有多個版本,每一個版本都有 row trx_id。
  2. 事務根據 undo log 和 trx_id 構建出知足當前隔離級別的一致性視圖。
  3. 可重複讀的核心是一致性讀,而事務更新數據的時候,只能使用當前讀,若是當前記錄的行鎖被其餘事務佔用,就須要進入鎖等待。

參考

03 | 事務隔離:爲何你改了我還看不見?-極客時間

08 | 事務究竟是隔離的仍是不隔離的?-極客時間

本文首發於個人我的博客 chaohang.top

做者 張小超

公衆號【超超不會飛】

轉載請註明出處

歡迎關注個人微信公衆號 【超超不會飛】,獲取第一時間的更新。

本文使用 mdnice 排版

相關文章
相關標籤/搜索