Mysql-MVCC

8 阅读4分钟

MVCC (Multi-Version Concurrency Control) 多版本并发控制

是什么

MVCC 是一种数据库并发控制机制,让读操作不加锁,通过保存数据的多个版本,实现读写互不阻塞。

核心目标:读不阻塞写,写不阻塞读,解决读写冲突问题。

怎么用

MVCC 是数据库引擎内部机制,不需要开发者手动使用。在 MySQL InnoDB 中:

  • 普通 SELECT 自动走 MVCC 快照读(不加锁)
  • 加锁读SELECT ... FOR UPDATE / LOCK IN SHARE MODE)走当前读(不走 MVCC)
-- 快照读,走MVCC,不加锁
SELECT * FROM account WHERE id = 1;

-- 当前读,不走MVCC,加锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;

-- 写操作也是当前读
UPDATE account SET balance = balance - 100 WHERE id = 1;

实现原理(InnoDB)

InnoDB 的 MVCC 依赖三个核心组件:

1. 隐藏字段

每行数据有两个隐藏列:

隐藏字段说明
DB_TRX_ID最后修改该行的事务 ID
DB_ROLL_PTR回滚指针,指向 undo log 中的上一个版本

2. Undo Log(版本链)

每次修改数据时,旧版本写入 undo log,通过 DB_ROLL_PTR 串成版本链

当前行 (trx_id=5, roll_ptr→)
  → 旧版本v2 (trx_id=3, roll_ptr→)
    → 旧版本v1 (trx_id=1, roll_ptr=NULL)

3. Read View(读视图)

事务执行快照读时生成,决定当前事务能看到哪个版本。包含四个关键字段:

字段说明
m_ids生成 Read View 时,所有活跃(未提交)事务 ID 列表
min_trx_id活跃事务中最小 ID
max_trx_id下一个将分配的事务 ID(即当前最大事务 ID + 1)
creator_trx_id创建该 Read View 的事务 ID

可见性判断规则

对版本链上某个版本,按以下顺序判断:

1. trx_id == creator_trx_id → 可见(自己修改的)
2. trx_id < min_trx_id → 可见(事务已提交)
3. trx_id >= max_trx_id → 不可见(事务在 Read View 之后才开始)
4. min_trx_id <= trx_id < max_trx_id:
   - trx_id 在 m_ids 中 → 不可见(事务未提交)
   - trx_id 不在 m_ids 中 → 可见(事务已提交)
5. 当前版本不可见 → 沿 roll_ptr 找上一个版本,重复判断

具体例子

场景:账户转账

CREATE TABLE account (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance INT
) ENGINE=InnoDB;

INSERT INTO account VALUES (1, 'Alice', 1000);  -- trx_id=1, 已提交

此时数据行:id=1, name='Alice', balance=1000, DB_TRX_ID=1, DB_ROLL_PTR=NULL

并发操作

时间线:
T1: 事务A (trx_id=2) 开始,未提交
T2: 事务B (trx_id=3) 开始,未提交
T3: 事务A UPDATE account SET balance=800 WHERE id=1;
    → 旧版本写入undo log,当前行 DB_TRX_ID=2, DB_ROLL_PTR→旧版本(balance=1000, trx_id=1)
T4: 事务B SELECT * FROM account WHERE id=1;  -- 快照读
T5: 事务C (trx_id=4) UPDATE account SET balance=600 WHERE id=1;  -- 等待事务A提交

T4 时刻事务B的 Read View:

  • m_ids = [2, 3](事务A、B都未提交)
  • min_trx_id = 2
  • max_trx_id = 5
  • creator_trx_id = 3

判断可见性:

  1. 当前行 trx_id=22 < min_trx_id(2)?否
  2. 2 >= max_trx_id(5)?否
  3. 2m_ids=[2,3] 中? → 不可见(事务A未提交)
  4. 沿 roll_ptr 找到旧版本 trx_id=1
  5. 1 < min_trx_id(2)可见

结果:事务B 读到 balance=1000(事务A修改前的值)

RC vs RR 的区别

隔离级别Read View 生成时机效果
RC(读已提交)每次 SELECT 都生成新 Read View能读到其他事务已提交的最新值
RR(可重复读)事务内第一次 SELECT 生成,后续复用事务内多次读结果一致
-- RR级别下
-- 事务B第一次SELECT → 生成Read View → 读到balance=1000
-- 事务A提交
-- 事务B第二次SELECT → 复用Read View → 仍然读到balance=1000(可重复读)

-- RC级别下
-- 事务B第一次SELECT → 生成Read View → 读到balance=1000
-- 事务A提交
-- 事务B第二次SELECT → 重新生成Read View → 读到balance=800(读已提交)

总结

维度说明
本质用版本链 + 读视图实现无锁读
核心组件隐藏字段 + Undo Log + Read View
优势读写不互斥,高并发性能好
局限Undo Log 占空间;长事务导致旧版本无法回收
适用MySQL InnoDB 的 RC/RR 隔离级别