MVCC 实现原理
🔴 困难题目描述
解释 MySQL InnoDB 的 MVCC(多版本并发控制)实现原理。Read View 是如何判断数据可见性的?
示例场景
-- 事务 A(trx_id=100)
START TRANSACTION;
SELECT * FROM users WHERE id = 1; -- 读取 balance=1000
-- 事务 B(trx_id=101)
START TRANSACTION;
UPDATE users SET balance = 900 WHERE id = 1;
COMMIT;
-- 事务 A
SELECT * FROM users WHERE id = 1; -- 仍然读到 balance=1000
COMMIT;提示
- MVCC 通过 Undo Log 实现多版本
- Read View 包含活跃事务列表
- 通过比较事务 ID 判断可见性
解法
参考答案 (3 个标签)
MVCC Read View Undo Log
MVCC 核心组件
1. 隐藏字段
-- InnoDB 每行数据有两个隐藏字段
CREATE TABLE users (
id INT PRIMARY KEY,
name VARCHAR(50),
balance INT
) ENGINE=InnoDB;
-- 实际存储:
-- | id | name | balance | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
-- | 1 | Alice| 1000 | 100 | NULL | 1 |
-- DB_TRX_ID:最后修改该行的事务 ID
-- DB_ROLL_PTR:回滚指针(指向 Undo Log)
-- DB_ROW_ID:隐藏主键(如果没有主键)2. Undo Log
-- 事务 A(trx_id=100)
START TRANSACTION;
UPDATE users SET balance = 1000 WHERE id = 1;
-- Undo Log 记录:
-- | 旧值 | 新值 |
-- | 900 | 1000 |
UPDATE users SET balance = 1100 WHERE id = 1;
-- Undo Log 链:
-- 最新值: balance=1100 (trx_id=100)
-- ↓ DB_ROLL_PTR
-- 旧值: balance=1000 (trx_id=100)
-- ↓ DB_ROLL_PTR
-- 旧值: balance=900 (trx_id=99)
-- ↓ DB_ROLL_PTR
-- NULL3. Read View
-- Read View 结构
struct ReadView {
m_ids; // 活跃事务列表(当前未提交的事务)
min_trx_id; // 活跃事务中最小 ID
max_trx_id; // 下一个要分配的事务 ID
creator_trx_id; // 创建该 Read View 的事务 ID
}
-- 示例:
-- 事务 A(trx_id=100)创建 Read View
-- m_ids = [98, 99, 101] -- 未提交的事务
-- min_trx_id = 98
-- max_trx_id = 102
-- creator_trx_id = 100可见性判断算法
-- 读取一行数据时:
-- 1. 获取该行的 DB_TRX_ID
-- 2. 与 Read View 比较
function isVisible(trx_id, read_view) {
if (trx_id == creator_trx_id) {
return true; // 自己修改的,可见
}
if (trx_id < min_trx_id) {
return true; // 事务已提交,可见
}
if (trx_id >= max_trx_id) {
return false; // 事务在 Read View 创建后才启动,不可见
}
if (trx_id in m_ids) {
return false; // 事务未提交,不可见
}
return true; // 事务已提交,可见
}MVCC 工作流程
-- 事务 A(trx_id=100)
START TRANSACTION;
-- 创建 Read View
-- m_ids = [98, 99]
-- min_trx_id = 98
-- max_trx_id = 101
-- creator_trx_id = 100
SELECT * FROM users WHERE id = 1;
-- balance=900 (trx_id=99)
-- 99 in m_ids → 不可见
-- 通过 DB_ROLL_PTR 找到旧版本
-- balance=1000 (trx_id=97)
-- 97 < min_trx_id → 可见
-- 返回 balance=1000
-- 事务 B(trx_id=101)
START TRANSACTION;
UPDATE users SET balance = 800 WHERE id = 1;
COMMIT;
-- 事务 A
SELECT * FROM users WHERE id = 1;
-- balance=800 (trx_id=101)
-- 101 >= max_trx_id → 不可见
-- 通过 DB_ROLL_PTR 找到旧版本
-- balance=900 (trx_id=99)
-- 99 in m_ids → 不可见
-- 继续找旧版本
-- balance=1000 (trx_id=97)
-- 97 < min_trx_id → 可见
-- 返回 balance=1000(可重复读)
COMMIT;扩展:RC 和 RR 的区别
READ COMMITTED
-- 每次查询都生成新的 Read View
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 1; -- Read View 1
-- m_ids = [98, 99]
-- 事务 B
START TRANSACTION;
UPDATE users SET balance = 800 WHERE id = 1;
COMMIT; -- trx_id=100
-- 事务 A
SELECT * FROM users WHERE id = 1; -- Read View 2(新生成)
-- m_ids = []
-- 100 not in m_ids → 可见
-- 返回 balance=800(不可重复读)
COMMIT;REPEATABLE READ
-- 只在第一次查询时生成 Read View
-- 事务 A
START TRANSACTION;
SELECT * FROM users WHERE id = 1; -- Read View(唯一)
-- m_ids = [98, 99]
-- 事务 B
START TRANSACTION;
UPDATE users SET balance = 800 WHERE id = 1;
COMMIT; -- trx_id=100
-- 事务 A
SELECT * FROM users WHERE id = 1; -- 使用同一个 Read View
-- m_ids = [98, 99]
-- 100 >= max_trx_id → 不可见
-- 返回 balance=900(可重复读)
COMMIT;Undo Log 的作用
1. 实现多版本
数据版本链:
最新版本:balance=1100 (trx_id=102)
↓ DB_ROLL_PTR
旧版本:balance=1000 (trx_id=100)
↓ DB_ROLL_PTR
旧版本:balance=900 (trx_id=99)
↓ DB_ROLL_PTR
旧版本:balance=1000 (trx_id=97)
↓ DB_ROLL_PTR
NULL2. 回滚事务
-- 事务回滚时
ROLLBACK;
-- 通过 Undo Log 恢复数据:
-- 1. 找到最新的 Undo Log
-- 2. 恢复旧值
-- 3. 释放锁3. MVCC 快照读
-- 快照读(普通 SELECT)
SELECT * FROM users WHERE id = 1;
-- 使用 Read View 和 Undo Log
-- 不加锁,读取快照
-- 当前读(加锁读)
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 读取最新版本
-- 加锁Purge 机制
-- 清理不再需要的 Undo Log
-- Purge 线程定期扫描:
-- 1. 找到最早的 Read View
-- 2. 删除该 Read View 之前的 Undo Log
-- 示例:
-- 活跃事务:[100, 101, 102]
-- 最早 Read View:min_trx_id = 100
-- 删除 trx_id < 100 的 Undo Log