【MySQL深入详解】第04篇:MVCC机制揭秘——MySQL如何实现高性能并发

2 阅读8分钟

写在前面:如果说锁是数据库的"交警",那么MVCC就是数据库的"时光机"。它让不同的事务可以看到不同时间点的数据快照,从而在保证一致性的同时实现高并发。这就是InnoDB能同时支持事务和高并发的秘密。

开篇引入:同一个世界,不同的视角

想象一个场景:你在看一部电影,同时有人在编辑这部电影。

传统锁机制的做法是:你看电影时,别人不能动剪刀。这样安全,但效率低。

MVCC的做法是:给你一个"电影胶片副本",你在看你的版本时,别人可以编辑原版。最终再把大家的修改合并。这既保证了你看电影时不会被打扰,又允许了并发编辑。

MySQL的InnoDB引擎就是用这种方式,在REPEATABLE READ隔离级别下,既保证了可重复读,又支持高并发。

MVCC是什么?

MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种并发控制机制。它通过保存数据的多个版本来实现非阻塞读操作。

核心思想

  • 每次修改数据时,不是直接覆盖原数据,而是创建一个新版本
  • 读操作不需要加锁,只读取符合条件的数据版本
  • 不同事务可能看到不同版本的数据
时间线 →
                    
  T1时刻  T2时刻  T3时刻  T4时刻
    │       │       │       │
    ▼       ▼       ▼       ▼
  [v1]    [v2]    [v2]    [v3]
           ↑               ↑
         Session A        Session B
         (读v2)          (读v3)

MVCC vs 传统锁对比

特性传统锁机制MVCC
读操作需要获取锁非阻塞读取
写操作需要等待读锁释放写锁+版本控制
并发度
复杂度
一致性通过锁保证通过快照+版本链保证

InnoDB的MVCC实现

关键概念

InnoDB在每行数据上额外存储了两个隐藏字段:

  1. DB_TRX_ID:最近一次修改该行的事务ID
  2. DB_ROLL_PTR:回滚指针,指向Undo Log中的旧版本

数据行的隐藏字段

-- InnoDB 行结构(概念上)
+--------+----------------------+----------------------+---------+
| 主键值  |  DB_TRX_ID (6字节)   | DB_ROLL_PTR (7字节)  | 其他列  |
+--------+----------------------+----------------------+---------+
|   1    |      12345           |       指向Undo       | name... |
+--------+----------------------+----------------------+---------+

Undo Log:版本链的链表

每条记录都有一个回滚指针,指向Undo Log中的旧版本,形成一条版本链:

当前记录 (id=1, name='Tom', tx_id=100)
       │
       │ DB_ROLL_PTR
       ↓
Undo Log记录 ← tx_id=99 (修改前name='John')
       │
       │ DB_ROLL_PTR
       ↓
Undo Log记录 ← tx_id=80 (修改前name='Alice')
       │
       ▼
      NULL

Read View:快照的眼睛

当事务开始读取数据时,MySQL会创建一个Read View(读取视图),这个视图包含了:

  1. m_ids:当前活跃事务的ID列表
  2. min_trx_id:最小活跃事务ID
  3. max_trx_id:创建Read View时最大事务ID+1
  4. creator_trx_id:当前事务ID

Read View的判断规则

读取数据时,通过以下规则判断是否可见:

如果 row.trx_id < min_trx_id:
    可见(事务已提交)

如果 row.trx_id >= max_trx_id:
    不可见(事务在Read View创建后开始)

如果 row.trx_id 在 m_ids 中:
    不可见(事务还在进行中)

否则:
    可见(事务已提交)

图解Read View

Read View 创建时刻
    ↓
活跃事务:[T1, T3, T5]  ← m_ids
最小事务ID:T1            ← min_trx_id
最大事务ID:T7            ← max_trx_id (T6+1)
当前事务ID:T4            ← creator_trx_id

某记录的 trx_id = T2:
    T2 < T1?不 → 进入下一步
    T2 在 [T1,T3,T5]?否 → 可见 ✓

某记录的 trx_id = T3:
    T3 < T1?不
    T3 在 [T1,T3,T5]?是 → 不可见 ✗

快照读 vs 当前读

这是理解MVCC的关键点。

快照读(Snapshot Read)

普通的SELECT语句就是快照读,它读取的是数据在某个时间点的快照。

-- 快照读:读取开始时的数据状态
SELECT * FROM users WHERE id = 1;
-- 返回事务开始时的数据

当前读(Current Read)

带有锁的读取是当前读,它读取的是最新版本的数据。

-- 当前读:读取最新数据,并加锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 当前读:读取最新数据,并加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;

-- 写入操作默认都是当前读
INSERT INTO users VALUES (1, 'Tom');
UPDATE users SET name = 'Jerry' WHERE id = 1;
DELETE FROM users WHERE id = 1;

为什么区分这两种读?

Session A                      Session B
────────                       ────────
BEGIN;
SELECT * FROM orders          BEGIN;
WHERE status = 'pending';      UPDATE orders SET status = 'completed'
                               WHERE id = 1;
                              COMMIT;
SELECT * FROM orders          
WHERE status = 'pending';
                              -- A 还能看到 pending 状态的订单
                              -- 因为它是快照读

MVCC在REPEATABLE READ下的行为

REPEATABLE READ是MySQL InnoDB的默认隔离级别。

核心规则

在同一事务中,多次执行相同的SELECT,结果是一致的。

-- Session A
START TRANSACTION;

-- 第一次读取
SELECT * FROM orders WHERE amount > 1000;
-- 结果:[order_1: 1500, order_2: 2000]

-- Session B(在A的两次读取之间)
START TRANSACTION;
INSERT INTO orders VALUES (3, 1, 3000);
COMMIT;

-- 第二次读取(REPEATABLE READ)
SELECT * FROM orders WHERE amount > 1000;
-- 结果:[order_1: 1500, order_2: 2000]  
-- 与第一次完全一致!B的插入对A不可见

Read View的创建时机

在REPEATABLE READ下,Read View在事务的第一次读取时创建。

事务开始
    ↓
第一次读取数据
    ↓
创建Read View(捕获当前活跃事务)
    ↓
后续所有读取都使用这个Read View
    ↓
事务结束

MVCC在READ COMMITTED下的行为

READ COMMITTED每次读取都会创建新的Read View。

-- Session A
START TRANSACTION;

-- 第一次读取
SELECT * FROM orders WHERE amount > 1000;
-- 创建Read View1: [T1]
-- 结果:[order_1: 1500]

-- Session B
INSERT INTO orders VALUES (2, 1, 2000);
COMMIT;

-- Session A 第二次读取
SELECT * FROM orders WHERE amount > 1000;
-- 创建Read View2: [T1](已更新,因为T2已提交)
-- 结果:[order_1: 1500, order_2: 2000]
-- 两次读取结果不同!

对比表

隔离级别Read View创建时机重复读取结果
REPEATABLE READ事务第一次读取一致
READ COMMITTED每次读取可能不同

MVCC解决幻读问题

幻读(Phantom Read)是指同一事务中,两次查询返回的记录数不一样。

没有MVCC的情况(假想)

Session A
START TRANSACTION;
SELECT COUNT(*) FROM orders WHERE user_id = 1;
-- 结果:5条

Session B
INSERT INTO orders VALUES (6, 1, 100);
COMMIT;

Session A
SELECT COUNT(*) FROM orders WHERE user_id = 1;
-- 结果:6条!产生了"幻觉"

InnoDB如何解决

InnoDB使用Next-Key Lock和MVCC组合来解决幻读:

  1. 写操作使用Next-Key Lock:锁定查询范围内的所有记录和间隙
  2. 读操作使用MVCC:通过Read View确保一致性
-- Session A
START TRANSACTION;
SELECT * FROM orders WHERE user_id = 1 FOR UPDATE;
-- 锁定 user_id=1 的所有记录及间隙

-- Session B
INSERT INTO orders VALUES (6, 1, 100);
-- 被阻塞!因为间隙被锁定

Session A
INSERT INTO orders VALUES (6, 1, 100);
-- A 可以插入,因为是自己锁定的间隙

MVCC的代价

MVCC带来了高并发,但也有代价。

1. 存储空间增加

每条记录都需要额外存储事务ID和回滚指针,Undo Log也会占用空间。

-- 查看Undo Log使用
SHOW ENGINE INNODB STATUS;
-- 关注 Undo tablespaces 相关信息

2. 清理成本

旧版本的Undo Log需要被清理,否则会无限增长。

清理过程:
1. purge线程定期检查
2. 判断Undo Log是否不再需要
3. 释放空间

条件:没有活跃事务需要读取该版本

3. 查询成本

每次读取都需要遍历版本链,找到可见的版本。

读取流程:
1. 检查当前记录是否可见
2. 不可见则跟随回滚指针
3. 重复直到找到可见版本或链尾

实战:理解MVCC的表现

实验1:验证REPEATABLE READ

-- Terminal 1
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
-- 结果:name='Alice'

-- Terminal 2
START TRANSACTION;
UPDATE users SET name = 'Bob' WHERE id = 1;
COMMIT;

-- Terminal 1
SELECT * FROM users WHERE id = 1;
-- 结果:name='Alice'(未变化!)
COMMIT;
-- 结果:name='Bob'(提交后可见新值)

实验2:验证READ COMMITTED

-- Terminal 1
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
-- 结果:name='Alice'

-- Terminal 2
START TRANSACTION;
UPDATE users SET name = 'Charlie' WHERE id = 1;
COMMIT;

-- Terminal 1
SELECT * FROM users WHERE id = 1;
-- 结果:name='Charlie'(变化了!)

实验3:验证当前读被阻塞

-- Terminal 1
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 锁定 id=1 的记录

-- Terminal 2
START TRANSACTION;
SELECT * FROM users WHERE id = 1;
-- 这个读不会阻塞(快照读)
-- 但如果用 FOR UPDATE 或 LOCK IN SHARE MODE,会阻塞

MVCC配置与监控

相关配置参数

-- 查看相关配置
SHOW VARIABLES LIKE 'innodb_undo%';

-- innodb_undo_directory:Undo Log存储目录
-- innodb_undo_tablespaces:Undo Log表空间数量
-- innodb_undo_log_truncate:是否自动截断Undo Log

监控MVCC相关指标

-- 查看事务状态
SELECT 
    trx_id,
    trx_state,
    trx_started,
    trx_rows_locked,
    trx_rows_modified
FROM information_schema.INNODB_TRX;

-- 查看锁等待
SHOW ENGINE INNODB STATUS;
-- 关注 Lock wait time 和 各事务锁信息

小结

MVCC是InnoDB实现高性能事务的关键技术。

核心要点

  1. MVCC核心思想:为每次修改保存新版本,读操作非阻塞
  2. 两个隐藏字段:事务ID + 回滚指针
  3. Undo Log版本链:通过回滚指针串联历史版本
  4. Read View:判断哪个版本对当前事务可见
  5. 快照读 vs 当前读:普通SELECT是快照读,FOR UPDATE/LOCK是当前读
  6. REPEATABLE READ:事务开始时创建Read View
  7. READ COMMITTED:每次读取创建新的Read View

实践建议

  • 理解快照读的"时间点"概念
  • 注意当前读会阻塞
  • 长事务会导致Undo Log膨胀
  • 合理设置Undo Log大小
  • 监控活跃事务和锁等待

上一篇【第03篇】事务的本质——ACID与隔离级别深度解读

下一篇【第05篇】MySQL监控体系——SLO驱动的可靠性工程


延伸阅读

  • 《高性能MySQL(第4版)》第1章 - MVCC部分
  • MySQL 8.0 Reference Manual - InnoDB Multi-Versioning
  • InnoDB Row Format
  • Understanding the InnoDB Transaction Model