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,从而影响数据的可见性。