MySQL 的 可重復讀(Repeatable Read, RR) 隔離級別主要通過 多版本并發控制(Multi-Version Concurrency Control, MVCC) 和 鎖機制(特別是間隙鎖) 來實現的。其核心目標是:在一個事務內,無論讀取多少次相同的行,看到的數據都是一致的(即第一次讀取時建立的“快照”),并且防止了“不可重復讀”問題(同一個事務內兩次讀取同一行得到不同值)。同時,在 InnoDB 的 RR 級別下,還通過間隙鎖機制(Next-Key Locks)極大地防止了“幻讀”問題(同一個事務內兩次范圍查詢得到不同的行集)。
以下是 RR 隔離級別實現的核心機制:
1. 多版本并發控制 (MVCC) - 實現快照讀 (Snapshot Read)
MVCC 是 RR 級別實現“可重復讀”特性的基石。
- 隱式字段: InnoDB 在每個聚簇索引記錄(數據行)中隱藏了 3 個關鍵字段:
DB_TRX_ID
(6 Bytes):記錄創建/最后修改該行的事務ID。 當一個事務開始修改某行時,會把自己的事務 ID 寫入這個字段。DB_ROLL_PTR
(7 Bytes):指向該行在 undo log 中的回滾段指針。 這個指針指向該行之前版本(舊版本)在 undo log 中的位置,形成一個版本鏈。DB_ROW_ID
(6 Bytes):單調遞增的行 ID(如果表沒有定義主鍵,InnoDB 會自動生成這個作為聚簇索引)。
- Undo Log (回滾日志): 當事務修改數據時:
- 會先將數據行的舊版本復制到 undo log 中。
- 然后修改數據行(更新
DB_TRX_ID
為當前事務 ID,更新DB_ROLL_PTR
指向剛寫入 undo log 的舊版本記錄)。 - 這樣,每個被修改的行都通過
DB_ROLL_PTR
鏈接成一個歷史版本鏈(鏈表)。
- ReadView (一致性視圖): 這是 MVCC 的關鍵數據結構,決定了事務能看到哪些版本的數據。當事務執行第一個
SELECT
語句(或顯式開啟只讀事務)時,InnoDB 會為該事務生成一個 ReadView。一個 ReadView 主要包含:m_ids
: 生成 ReadView 時,系統中活躍(未提交)的事務 ID 列表。min_trx_id
:m_ids
中的最小值。max_trx_id
: 生成 ReadView 時,系統應該分配給下一個新事務的 ID(即當前最大事務 ID + 1)。creator_trx_id
: 創建該 ReadView 的當前事務自己的 ID(只讀事務為 0)。
- 可見性規則: 當事務需要讀取一行時,它沿著該行的版本鏈(通過
DB_ROLL_PTR
回溯)查找第一個滿足以下條件的版本:- 版本對應的
DB_TRX_ID
<min_trx_id
:該版本是在 ReadView 創建之前就已提交的事務修改的。可見。 - 版本對應的
DB_TRX_ID
在m_ids
中:該版本是由 ReadView 創建時還活躍(未提交) 的事務修改的。不可見。繼續查找更舊的版本。 - 版本對應的
DB_TRX_ID
>=max_trx_id
:該版本是由 ReadView 創建之后才開啟的事務修改的。不可見。繼續查找更舊的版本。 - 版本對應的
DB_TRX_ID
=creator_trx_id
:該版本是由當前事務自身修改的。可見。
- 如果找到鏈頭(最初的版本)仍不可見,則認為該行對該事務不可見(如同不存在)。
- 版本對應的
- RR 級別的關鍵點:
- 在 RR 級別下,一個事務只在第一次執行
SELECT
時生成一個 ReadView。 - 后續在該事務內的所有 普通
SELECT
語句(快照讀) 都復用這個最初的 ReadView。 - 因此,無論之后其他事務如何修改、提交或回滾,該事務看到的數據始終是第一次
SELECT
時那個“快照”版本的數據。這就保證了“可重復讀”。
- 在 RR 級別下,一個事務只在第一次執行
2. 鎖機制 (Locking) - 處理當前讀 (Current Read) 和防止幻讀
MVCC 主要解決了快照讀(普通 SELECT
) 的可重復讀問題。但對于當前讀(如 SELECT ... FOR UPDATE
, SELECT ... LOCK IN SHARE MODE
, UPDATE
, DELETE
, INSERT
),以及需要防止幻讀,就需要用到鎖機制,特別是 Next-Key Locks。
- 行鎖 (Record Locks): 鎖定索引記錄本身。
- 間隙鎖 (Gap Locks): 鎖定索引記錄之間的間隙(一個開區間),防止其他事務在這個間隙中插入新記錄。
- 臨鍵鎖 (Next-Key Locks): 行鎖 + 間隙鎖的組合。鎖定索引記錄本身以及該記錄之前的間隙(一個左開右閉區間)。這是 InnoDB 在 RR 級別下默認使用的鎖類型。
- RR 級別下如何防止幻讀:
- 當一個事務執行當前讀(例如
SELECT * FROM t WHERE id > 100 FOR UPDATE
)時:- InnoDB 不僅會鎖住所有滿足條件
id > 100
的現有行(行鎖)。 - 還會在這些現有行的索引記錄范圍之后(以及可能的間隙之間)加上間隙鎖 (Gap Locks) 或 臨鍵鎖 (Next-Key Locks)。
- InnoDB 不僅會鎖住所有滿足條件
- 例如,如果現有 id 是 101, 105, 110。那么
id > 100
的查詢可能會鎖定:- 現有行:101, 105, 110(行鎖)
- 間隙:(100, 101), (101, 105), (105, 110)(間隙鎖)
- 以及最大值之后的間隙:(110, +∞)(間隙鎖)
- 這些間隙鎖會阻止其他事務在這些被鎖定的間隙中插入任何新的記錄(例如插入 id=102, 106, 115 等)。
- 因此,在該事務提交之前,其他事務無法插入滿足其查詢條件(
id > 100
)的新行。當該事務再次執行相同的范圍查詢時,就不會看到新插入的行(幻行),從而防止了幻讀。
- 當一個事務執行當前讀(例如
- 為什么 RR 級別能“極大防止”而非“絕對防止”幻讀?
- 快照讀 (
SELECT
): 基于 MVCC 的 ReadView,它看到的是固定快照,本身就不會看到其他事務新提交的數據(包括幻行)。所以快照讀天然不會發生幻讀。 - 當前讀 (
SELECT ... FOR UPDATE
等): 通過 Next-Key Locks 鎖住索引記錄和間隙,阻止了其他事務在鎖定范圍內插入新行,從而防止了當前讀的幻讀。 - “極大防止”的細微點: 如果一個事務 T1 只使用快照讀 (
SELECT
),它看不到其他事務插入的幻行(因為 MVCC)。但是,如果 T1 之后在同一個事務內執行了一個寫操作(如UPDATE
或DELETE
),而這個寫操作恰好影響到了其他事務新插入并提交的行(這些行對 T1 的快照讀是不可見的),那么 T1 的寫操作會“看到”這些新行(寫操作需要當前讀并可能加鎖)。如果這個寫操作修改了這些新行,那么 T1 后續的讀操作(即使是快照讀)再讀取這些被自己修改的行時,就會看到它們了。這種情況理論上構成了一種特殊的幻讀現象(寫操作發現了之前讀操作沒看到的行)。不過這種情況相對罕見,且很多業務場景下可以接受或規避。因此,通常說 InnoDB 的 RR 級別通過 Next-Key Locks “極大防止”了幻讀。
- 快照讀 (
總結 MySQL RR 隔離級別的實現
- MVCC (核心):
- 利用隱藏字段 (
DB_TRX_ID
,DB_ROLL_PTR
) 和 Undo Log 構建行數據的歷史版本鏈。 - 事務在第一次執行快照讀 (
SELECT
) 時生成一個 ReadView。 - 所有后續的快照讀都復用這個 ReadView,通過版本鏈的可見性規則訪問歷史快照數據,保證可重復讀。
- 利用隱藏字段 (
- Next-Key Locks (關鍵補充):
- 默認使用臨鍵鎖(行鎖 + 間隙鎖)。
- 在當前讀 (
SELECT ... FOR UPDATE/LOCK IN SHARE MODE
,UPDATE
,DELETE
,INSERT
) 時鎖定索引記錄及其前后的間隙。 - 阻止其他事務在鎖定范圍內插入新記錄,從而極大防止了幻讀的發生。
- Undo Log 的清理:
- 不再被任何活動事務的 ReadView 引用的舊版本數據(在 Undo Log 中)會被 Purge 線程清理掉。
因此,InnoDB 通過巧妙地結合 MVCC 的快照機制(處理讀) 和 Next-Key Locks 的鎖定機制(處理寫和范圍控制),高效且強有力地實現了可重復讀(RR)隔離級別的語義要求。