Photo by picography.co
《MySQL實戰45講》筆記。
簡單理解一下可重復讀
可重復讀是指:一個事務執行過程中看到的數據,總是跟這個事務在啟動時看到的數據是一致的。
我們可以簡單理解為:在可重復讀隔離級別下,事務在啟動的時候就”拍了個快照“。注意,這個快照是基於整個庫的。
這時,你可能就會想,如果一個庫有 100G,那么我啟動一個事務,MySQL就要拷貝 100G 的數據出來,這個過程得多慢啊。可是,我平時的事務執行起來很快啊。
實際上,我們並不需要拷貝出這 100G 的數據。我們來看下”快照“是怎么實現的。
拍個快照
InnoDB 里面每個事務都有一個唯一的事務 ID,叫作 transaction id。它在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。
每條記錄在更新的時候都會同時記錄一條 undo log,這條 log 就會記錄上當前事務的 transaction id,記為 row trx_id。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值。
如下圖所示,一行記錄被多個事務更新之后,最新值為 k=22。假設事務A在 trx_id=15 這個事務提交后啟動,事務A 要讀取該行時,就通過 undo log,計算出該事務啟動瞬間該行的值為 k=10。
在可重復讀隔離級別下,一個事務在啟動時,InnoDB 會為事務構造一個數組,用來保存這個事務啟動瞬間,當前正在”活躍“的所有事務ID。”活躍“指的是,啟動了但還沒提交。
數組里面事務 ID 為最小值記為低水位,當前系統里面已經創建過的事務 ID 的最大值加 1 記為高水位。
這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。
這個視圖數組把所有的 row trx_id 分成了幾種不同的情況。
- 如果 trx_id 小於低水位,表示這個版本在事務啟動前已經提交,可見;
- 如果 trx_id 大於高水為,表示這個版本在事務啟動后生成,不可見;
- 如果 trx_id 大於低水位,小於高水位,分為兩種情況:
- 若 trx_id 在數組中,表示這個版本在事務啟動時還未提交,不可見;
- 若 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]。
- 事務C通過更新語句,把 k 更新為 2,此時trx_id=102;
- 事務B通過更新語句,把 k 更新為 3,此時trx_id=101;
- 事務B通過查詢語句,查詢到最新一條記錄為3,trx_id=101,滿足隔離條件,返回 k=3;
- 事務A通過查詢語句:
- 查詢到最新一條記錄為3,trx_id=101,比高水位大,不可見;
- 通過 undo log,找到上一個歷史版本,trx_id=102,比高水位大,不可見;
- 繼續找上一個歷史版本,trx_id=90,比低水位小,可見。
提出問題:為啥事務B更新的時候能看到事務C的修改?
我們假設事務B在更新的看不到事務C的修改,是什么個情況?
- 事務B查詢到最新一條記錄為2,trx_id=102,比高水位大,不可見;
- 通過 undo log,找到上一個版本,trx_id=90,比低水位小,可見;
- 返回記錄 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:
- 事務B把 k 更新為 2;
- 事務C讀取到當前最新值,k=2,更新為3;
- 事務B回滾;
- 事務C提交。
這時候,事務C發現自己想要執行的是 +1 操作,結果變成了 ”+2“ 操作。
InnoDB 肯定不允許這種情況的發生,事務B在執行更新語句時,會給該行加上行鎖,直到事務B結束,才會釋放這個鎖。
小結
- InnoDB 的行數據有多個版本,每個版本都有 row trx_id。
- 事務根據 undo log 和 trx_id 構建出滿足當前隔離級別的一致性視圖。
- 可重復讀的核心是一致性讀,而事務更新數據的時候,只能使用當前讀,如果當前記錄的行鎖被其他事務占用,就需要進入鎖等待。
參考
本文首發於我的個人博客 https://chaohang.top
作者 張小超
公眾號【超超不會飛】
轉載請注明出處
歡迎關注我的微信公眾號 【超超不會飛】,獲取第一時間的更新。