MySQL MVCC 機制詳解
1. MVCC 基本概念
MVCC 是一種并發控制的方法,主要用于數據庫管理系統,允許多個事務同時讀取數據庫中的同一個數據項,而不需要加鎖,從而提高了數據庫的并發性能。
┌─────────────────────────────────────┐
│ MVCC 的核心思想 │
│ │
│ 對數據進行修改操作時,不會直接覆蓋數據,│
│ 而是創建一個新版本,讓讀操作可以看到 │
│ 修改前的數據 │
└─────────────────────────────────────┘
2. MVCC 在 InnoDB 中的實現
InnoDB 引擎下 MVCC 的實現主要基于以下幾個關鍵概念:
2.1 隱藏字段
InnoDB 為每一行記錄添加了三個隱藏字段:
┌───────────────────────────────────────────────────────┐
│ InnoDB 行記錄結構 │
├───────────┬───────────┬───────────┬───────────────────┤
│ DB_TRX_ID │ DB_ROLL_PTR│ DB_ROW_ID │ 實際數據列(可見部分) │
│ 事務ID │ 回滾指針 │ 行ID(可選) │ │
└───────────┴───────────┴───────────┴───────────────────┘
- DB_TRX_ID:創建或最后修改該記錄的事務ID
- DB_ROLL_PTR:指向 undo log 的指針,用于數據回滾
- DB_ROW_ID:如果沒有主鍵,InnoDB 會自動生成的行ID
2.2 undo log 版本鏈
當一行記錄被修改時,InnoDB 會將舊版本的記錄寫入 undo log,并在當前記錄中通過回滾指針指向這個 undo log 記錄,形成一個版本鏈。
┌──────────────────────────────────────────────────────────────┐
│ 版本鏈示意圖 │
│ │
│ 最新記錄 │
│ ┌─────────┬─────────┬─────────┬───────────┐ │
│ │TRX_ID=30│ROLL_PTR │ROW_ID │name="張三" │ │
│ └─────────┴─────────┴─────────┴───────────┘ │
│ │ │
│ ▼ 回滾指針指向 │
│ Undo Log 1 │
│ ┌─────────┬─────────┬─────────┬───────────┐ │
│ │TRX_ID=20│ROLL_PTR │ROW_ID │name="李四" │ │
│ └─────────┴─────────┴─────────┴───────────┘ │
│ │ │
│ ▼ 回滾指針指向 │
│ Undo Log 2 │
│ ┌─────────┬─────────┬─────────┬───────────┐ │
│ │TRX_ID=10│ROLL_PTR │ROW_ID │name="王五" │ │
│ └─────────┴─────────┴─────────┴───────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
2.3 ReadView
ReadView 是 MVCC 實現的關鍵機制,它決定了當前事務能夠看到哪個版本的數據。ReadView 包含以下重要信息:
- m_ids:當前系統中活躍的事務ID集合
- min_trx_id:活躍的最小事務ID
- max_trx_id:系統中將要分配給下一個事務的ID
- creator_trx_id:創建該 ReadView 的事務ID
┌──────────────────────────────────────────────┐
│ ReadView 示意圖 │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ m_ids: [10, 20, 30] │ │
│ │ min_trx_id: 10 │ │
│ │ max_trx_id: 40 │ │
│ │ creator_trx_id: 25 │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────┘
3. MVCC 可見性判斷規則
當一個事務要讀取一行記錄時,它會根據 ReadView 和記錄的 DB_TRX_ID 來判斷該版本的記錄是否可見:
┌─────────────────────────────────────────────────────────────────┐
│ MVCC 可見性判斷流程圖 │
│ │
│ ┌───────────────┐ │
│ │ 開始判斷可見性 │ │
│ └───────┬───────┘ │
│ ▼ │
│ ┌───────────────────────────────┐ 是 ┌───────────┐ │
│ │ trx_id == creator_trx_id? ├────────────?│ 可見 │ │
│ └───────────┬───────────────────┘ └───────────┘ │
│ │ 否 │
│ ▼ │
│ ┌───────────────────────────────┐ 是 ┌───────────┐ │
│ │ trx_id < min_trx_id? ├────────────?│ 可見 │ │
│ └───────────┬───────────────────┘ └───────────┘ │
│ │ 否 │
│ ▼ │
│ ┌───────────────────────────────┐ 是 ┌───────────┐ │
│ │ trx_id >= max_trx_id? ├────────────?│ 不可見 │ │
│ └───────────┬───────────────────┘ └───────────┘ │
│ │ 否 │
│ ▼ │
│ ┌───────────────────────────────┐ 是 ┌───────────┐ │
│ │ trx_id 在 m_ids 中? ├────────────?│ 不可見 │ │
│ └───────────┬───────────────────┘ └───────────┘ │
│ │ 否 │
│ ▼ │
│ ┌───────────┐ │
│ │ 可見 │ │
│ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
4. 不同隔離級別下的 MVCC 行為
MVCC 主要在 READ COMMITTED 和 REPEATABLE READ 隔離級別下工作:
┌─────────────────────────────────────────────────────────────────┐
│ 不同隔離級別的 ReadView 創建時機 │
│ │
│ ┌────────────────────┐ ┌─────────────────────────────┐ │
│ │ READ COMMITTED │ │ REPEATABLE READ │ │
│ ├────────────────────┤ ├─────────────────────────────┤ │
│ │ │ │ │ │
│ │ 每次SELECT時創建新的 │ │ 事務開始時創建一次ReadView │ │
│ │ ReadView │ │ 之后所有查詢復用這個ReadView │ │
│ │ │ │ │ │
│ └────────────────────┘ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
4.1 READ COMMITTED
- 每次SELECT都會創建一個新的ReadView
- 可以看到其他已提交事務的更改
- 解決了臟讀問題,但可能出現不可重復讀
4.2 REPEATABLE READ(MySQL默認)
- 在事務開始時創建一個ReadView,之后的查詢都復用這個ReadView
- 在整個事務過程中,對已經讀取的數據,其他事務的更新對當前事務不可見
- 解決了不可重復讀問題
5. MVCC的實際例子
假設我們有一個簡單的表格和以下操作序列:
創建表: CREATE TABLE user(id INT PRIMARY KEY, name VARCHAR(20));
初始數據: INSERT INTO user VALUES(1, '小明');
接下來,有三個事務同時操作這條記錄:
┌─────────────────────────────────────────────────────────────────┐
│ 事務并發執行示例 │
│ │
│ 時間 │ 事務A(trx_id=10) │ 事務B(trx_id=20) │ 事務C(trx_id=30) │
│ ─────┼──────────────────┼──────────────────┼────────────────── │
│ t1 │ BEGIN; │ │ │
│ t2 │ │ BEGIN; │ │
│ t3 │ │ │ BEGIN; │
│ t4 │ SELECT * FROM │ │ │
│ │ user WHERE id=1; │ │ │
│ │ 結果: '小明' │ │ │
│ t5 │ │ UPDATE user SET │ │
│ │ │ name='小紅' │ │
│ │ │ WHERE id=1; │ │
│ t6 │ │ COMMIT; │ │
│ t7 │ SELECT * FROM │ │ │
│ │ user WHERE id=1; │ │ │
│ │ 結果(RC): '小紅' │ │ │
│ │ 結果(RR): '小明' │ │ │
│ t8 │ │ │ UPDATE user SET │
│ │ │ │ name='小黑' │
│ │ │ │ WHERE id=1; │
│ t9 │ │ │ COMMIT; │
│ t10 │ SELECT * FROM │ │ │
│ │ user WHERE id=1; │ │ │
│ │ 結果(RC): '小黑' │ │ │
│ │ 結果(RR): '小明' │ │ │
│ t11 │ COMMIT; │ │ │
└─────────────────────────────────────────────────────────────────┘
版本鏈變化過程:
初始狀態:
┌─────────┬─────────┬─────────┬───────────┐
│TRX_ID=1 │ROLL_PTR │ROW_ID │name="小明" │
└─────────┴─────────┴─────────┴───────────┘事務B更新后:
┌─────────┬─────────┬─────────┬───────────┐
│TRX_ID=20│ROLL_PTR │ROW_ID │name="小紅" │
└─────────┴─────────┴─────────┴───────────┘│▼
┌─────────┬─────────┬─────────┬───────────┐
│TRX_ID=1 │ROLL_PTR │ROW_ID │name="小明" │
└─────────┴─────────┴─────────┴───────────┘事務C更新后:
┌─────────┬─────────┬─────────┬───────────┐
│TRX_ID=30│ROLL_PTR │ROW_ID │name="小黑" │
└─────────┴─────────┴─────────┴───────────┘│▼
┌─────────┬─────────┬─────────┬───────────┐
│TRX_ID=20│ROLL_PTR │ROW_ID │name="小紅" │
└─────────┴─────────┴─────────┴───────────┘│▼
┌─────────┬─────────┬─────────┬───────────┐
│TRX_ID=1 │ROLL_PTR │ROW_ID │name="小明" │
└─────────┴─────────┴─────────┴───────────┘
6. MVCC的優缺點
優點:
- 提高并發性能,讀不阻塞寫,寫不阻塞讀
- 解決了讀-寫沖突問題
- 支持事務的隔離級別實現
缺點:
- 需要額外的存儲空間維護舊版本數據
- 需要定期清理過時的舊版本數據
- 實現較為復雜
總結
MVCC 是 MySQL InnoDB 存儲引擎中實現高并發的關鍵技術,通過在每行記錄后面保存兩個隱藏的列(事務ID和回滾指針)來實現的。它能夠讓不同事務的讀、寫操作并發執行,同時保證事務的隔離性。根據不同的隔離級別,MySQL 會采用不同的策略來創建和維護 ReadView,從而影響數據的可見性。