MySQL MVCC 机制:多版本并发控制的实现与原理
MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 实现高并发读写的核心机制,其核心思想是通过保存数据的多个版本,让读写操作互不阻塞,从而提升数据库的并发性能。MVCC 主要支撑了 InnoDB 的READ COMMITTED(读已提交)和REPEATABLE READ(可重复读)隔离级别。
MVCC 的核心目标
在并发场景下,数据库需要解决两个核心问题:
- 读不阻塞写:读操作无需等待写操作释放锁。
- 写不阻塞读:写操作无需等待读操作释放锁。
MVCC 通过为数据维护多个版本,让读操作访问历史版本,写操作创建新版本,从而实现读写并行,避免传统锁机制下的性能损耗。
MVCC 的实现基础
InnoDB 实现 MVCC 依赖两个核心组件:数据的隐藏列和undo log(回滚日志)。
数据的隐藏列
InnoDB 的每条记录除了用户定义的字段外,还隐含 4 个隐藏列,用于维护版本信息:
| 隐藏列 | 含义 |
|---|---|
| DB_ROW_ID | 隐含的自增主键(若表没有显式定义主键,InnoDB 会自动生成),用于唯一标识记录。 |
| DB_TRX_ID | 最近一次修改该记录的事务 ID(事务 ID 是全局唯一、递增的)。 |
| DB_ROLL_PTR | 回滚指针,指向该记录的上一个版本(存储在 undo log 中)。 |
| FLAG | 删除标记(记录被删除时,不会立即物理删除,而是标记为删除,后续异步清理)。 |
undo log 与版本链
当事务修改记录时,InnoDB 会将修改前的旧版本数据存入 undo log,并通过DB_ROLL_PTR将新记录与旧记录关联,形成版本链(链表结构)。
- 版本链的链首是记录的最新版本(当前有效版本)。
- 版本链的链尾是记录的最早版本(可能被多次修改)。
示例:
事务 1 插入一条记录,事务 2、3 先后修改该记录,版本链结构如下:
1 | 当前记录(最新版本) |
ReadView:版本可见性判断的核心
ReadView(读视图)是 MVCC 中判断数据版本是否可见的关键机制,它记录了当前事务执行查询时,系统中 “活跃事务” 的状态。通过 ReadView,事务可以确定哪些版本的记录对自己可见。
ReadView 的核心字段
ReadView 包含 4 个核心字段:
| 字段名 | 含义 |
|---|---|
| m_ids | 当前活跃的事务 ID 集合(即查询时未提交的事务 ID)。 |
| min_trx_id | m_ids 中的最小事务 ID(当前活跃事务的最小 ID)。 |
| max_trx_id | 系统下一个将分配的事务 ID(即已创建的最大事务 ID + 1)。 |
| creator_trx_id | 创建该 ReadView 的事务 ID(当前查询所属的事务 ID)。 |
版本可见性规则
对于版本链中的某条记录(版本对应的事务 ID 为trx_id),ReadView 通过以下规则判断其是否可见:
- 若
trx_id == creator_trx_id:该版本由当前事务修改,可见。 - 若
trx_id < min_trx_id:该版本由 “已提交的事务” 修改(修改时无活跃事务),可见。 - 若
trx_id > max_trx_id:该版本由 “未来的事务” 修改(查询时尚未创建),不可见。 - 若
min_trx_id ≤ trx_id ≤ max_trx_id:- 若
trx_id在m_ids中(该事务仍活跃未提交),不可见; - 若
trx_id不在m_ids中(该事务已提交),可见。
- 若
不同隔离级别的 ReadView 生成时机
ReadView 的生成时机决定了隔离级别的行为,这是READ COMMITTED和REPEATABLE READ的核心差异:
- READ COMMITTED(RC):
每次执行SELECT时,都会重新生成一个 ReadView。因此,同一事务中多次查询可能看到不同的结果(可读取到其他事务已提交的修改)。 - REPEATABLE READ(RR):
仅在事务第一次执行SELECT时生成 ReadView,后续所有查询复用该 ReadView。因此,同一事务中多次查询结果一致(可重复读)。
MVCC 工作流程示例
以REPEATABLE READ隔离级别为例,通过具体场景理解 MVCC 的工作流程:
场景说明:
- 事务 1(ID=100):插入一条记录(name = 张三)。
- 事务 2(ID=200):查询该记录。
- 事务 3(ID=300):修改该记录(name = 李四)并提交。
- 事务 2 再次查询该记录。
步骤拆解:
- 事务 1 插入记录:
记录的DB_TRX_ID=100,DB_ROLL_PTR=NULL(无旧版本)。 - 事务 2 首次查询(生成 ReadView):
- 此时活跃事务:仅事务 2(ID=200)自身(假设事务 3 未开始)。
- ReadView:
m_ids=[200],min_trx_id=200,max_trx_id=301,creator_trx_id=200。 - 检查记录版本(
trx_id=100):100 < min_trx_id(200)→ 可见,查询结果为 “张三”。
- 事务 3 修改记录并提交:
- 记录最新版本的
DB_TRX_ID=300,DB_ROLL_PTR指向旧版本(trx_id=100)。 - 事务 3 提交后,从活跃事务集合中移除。
- 记录最新版本的
- 事务 2 再次查询(复用 ReadView):
- 仍使用首次生成的 ReadView(
m_ids=[200],min_trx_id=200)。 - 检查最新版本(
trx_id=300):300 > max_trx_id(301)不成立,且300不在m_ids中,但300 > min_trx_id(200)→ 不可见。 - 沿版本链查找旧版本(
trx_id=100):符合可见规则 → 查询结果仍为 “张三”(实现可重复读)。
- 仍使用首次生成的 ReadView(
MVCC 的优势与局限
优势:
- 读写不冲突:读操作访问历史版本,无需加锁;写操作仅锁定当前版本,提高并发性能。
- 隔离级别支撑:通过 ReadView 的生成时机,灵活支撑 RC 和 RR 隔离级别。
局限:
- undo log 膨胀:版本链可能积累大量旧版本,需依赖 InnoDB 的 purge 机制清理(异步删除已无用的旧版本)。
- 仅适用于特定隔离级别:
READ UNCOMMITTED(总是读最新版本)和SERIALIZABLE(表级锁)不依赖 MVCC