导航菜单

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
-- NULL

3. 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
NULL

2. 回滚事务

-- 事务回滚时
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

搜索