MySQL -- RR隔離與RC隔離
- 虛擬表 – 本文不關心
- 在呼叫的時候執行 查詢語句 並生成執行結果
- SQL語句:
CREATE VIEW
- InnoDB在實現 MVCC 時用到的 一致性讀檢視 (consistent read view)
- 用於支援 RC 和 RR 隔離級別的實現
- 沒有對應的物理結構
- 主要作用:在事務執行期間,事務能看到怎樣的資料
快照
- 在 RR 隔離級別下,事務在啟動的時候儲存了一個 快照 ,快照是基於 整庫 的
- 在InnoDB,每個事務都有一個 唯一的事務ID ( transaction id )
- 在 事務開始 的時候向InnoDB的 事務系統 申請的, 按申請的順序嚴格遞增
- 每行資料都有 多個版本 ,每次事務 更新資料 的時候,都會生成一個 新的資料版本
- 事務會把自己的 transaction id 賦值給這個資料版本的事務ID,記為
row trx_id
- 每個資料版本都有對應的row trx_id
- 同時也要 邏輯保留 舊的資料版本,通過新的資料版本和
undolog
可以 計算 出舊的資料版本
- 事務會把自己的 transaction id 賦值給這個資料版本的事務ID,記為
多版本
- 虛線框是同一行記錄的4個版本
- 當前最新版本為V4,k=22,是被
transaction id
為25的事務所更新的,因此它的row trx_id
為25 - 虛線箭頭就是
undolog
,而V1、V2和V3並 不是物理真實存在 的- 每次需要的時候根據 當前最新版本 與
undolog
計算出來的 - 例如當需要V2時,就通過V4依次執行U3和U2算出來的
- 每次需要的時候根據 當前最新版本 與
建立快照
- RR的定義:在事務啟動時,能夠看到 所有已經提交的事務結果
- 在該事務後續的執行過程中,其他事務的更新對該事務是不可見的
- 在事務啟動時,事務 只認可在該事務啟動之前提交的資料版本
- 在實現上,InnoDB會為每個事務構造一個 檢視陣列 ,用來儲存在這個事務啟動的瞬間,所有處於 活躍狀態 的事務ID
- 活躍的定義: 啟動了但尚未提交
- 低水位與高水位
- 低水位 :檢視數組裡面 最小的事務ID
- 高水位 :當前系統中 已經建立過最大事務ID+1 ,一般就是當前事務的
transaction id
- 當前事務的 一致性讀檢視 的組成部分: 檢視陣列 和 高水位
- 獲取事務的檢視陣列和高水位在 事務系統的鎖保護 下進行,可以認為是 原子 操作,期間 不能建立事務
- InnoDB利用了資料的 Multi-Version 的特性,實現 快照的秒級建立
- 快照 = 一致性讀檢視 = 檢視陣列+高水位
事務啟動
-
BEGIN/START TRANSACTION
:事務 並未立馬啟動 ,在執行到後續的第一個 一致性讀 語句,事務才真正開始 -
START TRANSACTION WITH CONSISTENT SNAPSHOT;
:事務 立馬啟動
樣例分析
表初始化
# 建表 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);
樣例1
事務執行流程
事務ABC的執行流程( autocommit=1 )
事務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的查詢
假設
- 事務A開始前,系統裡只有一個活躍事務ID是99
- 事務ABC的事務ID分別是100,101和102,且當前系統只有這4個事務
- 事務ABC開始前,
(1,1)
這一行資料的row trx_id
是90 - 檢視陣列
[99,100] [99,100,101] [99,100,101,102]
- 低水位與高水位
- 事務A:
99
和100
- 事務B:
99
和101
- 事務C:
99
和102
- 事務A:
查詢邏輯
- 第一個有效更新是事務C,採用 當前讀 ,讀取當前最新版本
(1,1)
,改成(1,2)
- 此時最新版本的
row trx_id
為102,90那個版本成為歷史版本 - 由於 autocommit=1 ,事務C在執行完更新後會立馬 釋放 id=1的 行鎖
- 此時最新版本的
- 第二個有效更新是事務B,採用 當前讀 ,讀取當前最新版本
(1,2)
,改成(1,3)
- 此時最新版本的
row trx_id
為101,102那個版本成為歷史版本
- 此時最新版本的
- 事務A查詢時,由於事務B還未提交,當前最新版本為
(1,3)
,對事務A是不可見的,否則就了 髒讀 了,讀取過程如下- 事務A的檢視陣列為
[99,100]
,讀資料都是從 當前最新版本 開始讀 - 首先找到當前最新版本
(1,3)
,判斷row trx_id
為101,比事務A的檢視陣列的高水位(100)大, 不可見 - 接著尋找 上一歷史版本 ,判斷
row trx_id
為102,同樣比事務A的檢視陣列的高水位(100)大, 不可見 - 再往前尋找,找到版本
(1,1)
,判斷row trx_id
為90,比事務A的檢視陣列的低水位(99)小, 可見 - 所以事務A的查詢結果為1
- 事務A的檢視陣列為
- 一致性讀 :事務A不論在什麼時候查詢,看到的資料都是 一致 的,哪怕同一行資料同時會被其他事務更新
時間視角
- 一個 資料版本 ,對於一個 事務檢視 來說,除了該事務本身的更新總是可見以外,還有下面3種情況
- 如果版本對應的事務未提交,不可見
- 如果版本對應的事務已提交,但是是在檢視建立之後提交的,不可見
- 如果版本對應的事務已提交,並且是在檢視建立之前提交的,可見
- 歸納: 一個事務只承認自身更新的資料版本以及檢視建立之前已經提交的資料版本
- 應用規則進行分析
- 事務A的 一致性讀檢視 是在事務A啟動時生成的,在事務A查詢時
- 此時
(1,3)
的資料版本尚未提交,不可見 - 此時
(1,2)
的資料版本雖然提交了,但是是在事務A的 一致性讀檢視 建立之後提交的,不可見 - 此時
(1,1)
的資料版本是在事務A的 一致性讀檢視 建立之前提交的,可見
更新邏輯
- 如果在事務B執行更新之前查詢一次,採用的是 一致性讀 ,查詢結果也為1
- 如果事務B要執行更新操作,是 不能在歷史版本上更新
- 否則事務C的更新就會 丟失 ,或者需要採取分支策略來相容(增加複雜度)
- 因此更新資料需要先進行 當前讀 (current read),再寫入資料
- 當前讀:總是讀取已經提交的最新版本
- 當前讀伴隨著加鎖 (更新操作為 X Lock模式的當前讀 )
- 如果當前事務在執行當前讀時,其他事務在這之前已經執行了更新操作,但尚未提交( 持有行鎖 ),當前事務被阻塞
- 事務B的
SET k=k+1
操作是在最新版(1,2)
上進行的,更新後生成新的資料版本(1,3)
,對應的row trx_id
為101 - 事務B在進行後續的查詢時,發現最新的資料版本為
101
,與自己的版本號 一致 ,認可該資料版本,查詢結果為3
當前讀
# 查詢語句 ## 讀鎖(S鎖,共享鎖) SELECT k FROM t WHERE id=1 LOCK IN SHARE MODE; ## 寫鎖(X鎖,排他鎖) SELECT k FROM t WHERE id=1 FOR UPDATE; # 更新語句,首先採用(X鎖的)當前讀
樣例2
事務執行流程
事務ABC’的執行流程
事務A | 事務B | 事務C’ |
---|---|---|
START TRANSACTION WITH CONSISTENT SNAPSHOT; | ||
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; | ||
COMMIT; | ||
SELECT k FROM t WHERE id=1; | ||
COMMIT; | ||
COMMIT; |
- 事務C’沒有自動提交,依然持有當前最新版本版本
(1,2)
上的 寫鎖 (X Lock) - 事務B執行更新語句,採用的是 當前讀 (X Lock模式),會被阻塞,必須等事務C’釋放這把寫鎖後,才能繼續執行
樣例3
# 建表 CREATE TABLE `t` ( `id` INT(11) NOT NULL, `c` INT(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; # 表初始化 INSERT INTO t (id, c) VALUES (1,1),(2,2),(3,3),(4,4);
事務執行順序1
session A | session B |
---|---|
BEGIN; | |
SELECT * FROM T; | |
UPDATE t SET c=c+1 | |
UPDATE t SET c=0 WHERE id=c; | |
SELECT * FROM T; |
事務執行順序2
session A | session B’ |
---|---|
BEGIN; | |
SELECT * FROM T; | |
BEGIN; | |
SELECT * FROM T; | |
UPDATE t SET c=c+1; | |
COMMIT; | |
UPDATE t SET c=0 WHERE id=c; | |
SELECT * FROM T; |
session A視角
mysql> BEGIN; Query OK, 0 rows affected (0.00 sec) mysql> SELECT * FROM t; +----+------+ | id | c| +----+------+ |1 |1 | |2 |2 | |3 |3 | |4 |4 | +----+------+ 4 rows in set (0.00 sec) mysql> UPDATE t SET c=0 WHERE id=c; Query OK, 0 rows affected (0.01 sec) Rows matched: 0Changed: 0Warnings: 0 # 沒有修改成功,因為update時採用當前讀,基於最新的資料版本(已被其他事務修改並提交) mysql> SELECT * FROM t; +----+------+ | id | c| +----+------+ |1 |1 | |2 |2 | |3 |3 | |4 |4 | +----+------+ 4 rows in set (0.00 sec)
RR與RC
RR
- RR的實現核心為 一致性讀 (consistent read)
- 事務更新資料的時候,只能用 當前讀 (current read)
- 如果當前的記錄的行鎖被其他事務佔用的話,就需要進入 鎖等待
- 在RR隔離級別下,只需要在事務 啟動 時建立一致性讀檢視,之後事務裡的其他查詢都共用這個一致性讀檢視
- 對於RR,查詢只承認 事務啟動前 就已經提交的資料
- 表結構不支援RR,只支援當前讀
- 因為表結構沒有對應的行資料,也沒有row trx_id
RC
- 在RC隔離級別下,每個 語句執行前 都會 重新計算 出一個新的一致性讀檢視
- 在RC隔離級別下,再來考慮樣例1,事務A與事務B的查詢語句的結果
-
START TRANSACTION WITH CONSISTENT SNAPSHOT
的原意:建立一個 持續整個事務 的 一致性檢視- 在RC隔離級別下,一致性讀檢視會被 重新計算 ,等同於普通的
START TRANSACTION
- 在RC隔離級別下,一致性讀檢視會被 重新計算 ,等同於普通的
- 事務A的查詢語句的一致性讀檢視是在執行這個語句時才建立的
- 資料版本
(1,3)
未提交,不可見 - 資料版本
(1,2)
提交了,並且在事務A 當前的一致性讀檢視 建立之前提交的, 可見 - 因此事務A的查詢結果為2
- 資料版本
- 事務B的查詢結果為3
- 對於RC,查詢只承認 語句啟動前 就已經提交的資料
參考資料
《MySQL實戰45講》
轉載請註明出處:http://zhongmingmao.me/2019/01/28/mysql-transaction-isolation-rr-rc/
訪問原文「 MySQL -- RR隔離與RC隔離 」獲取最佳閱讀體驗並參與討論