0%

MVCC机制

MySQL MVCC 机制:多版本并发控制的实现与原理

MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 实现高并发读写的核心机制,其核心思想是通过保存数据的多个版本,让读写操作互不阻塞,从而提升数据库的并发性能。MVCC 主要支撑了 InnoDB 的READ COMMITTED(读已提交)和REPEATABLE READ(可重复读)隔离级别。

MVCC 的核心目标

在并发场景下,数据库需要解决两个核心问题:

  1. 读不阻塞写:读操作无需等待写操作释放锁。
  2. 写不阻塞读:写操作无需等待读操作释放锁。

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
2
3
4
5
6
7
8
9
10
11
当前记录(最新版本)
DB_TRX_ID=3(事务3修改)
DB_ROLL_PTR → 指向事务2修改后的版本

事务2修改后的版本
DB_TRX_ID=2(事务2修改)
DB_ROLL_PTR → 指向事务1插入的版本

事务1插入的版本(最早版本)
DB_TRX_ID=1(事务1插入)
DB_ROLL_PTR → NULL(无更早版本)

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 通过以下规则判断其是否可见:

  1. trx_id == creator_trx_id:该版本由当前事务修改,可见。
  2. trx_id < min_trx_id:该版本由 “已提交的事务” 修改(修改时无活跃事务),可见。
  3. trx_id > max_trx_id:该版本由 “未来的事务” 修改(查询时尚未创建),不可见。
  4. min_trx_id ≤ trx_id ≤ max_trx_id
    • trx_idm_ids中(该事务仍活跃未提交),不可见;
    • trx_id不在m_ids中(该事务已提交),可见。

不同隔离级别的 ReadView 生成时机

ReadView 的生成时机决定了隔离级别的行为,这是READ COMMITTEDREPEATABLE 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. 事务 1 插入记录
    记录的DB_TRX_ID=100DB_ROLL_PTR=NULL(无旧版本)。
  2. 事务 2 首次查询(生成 ReadView)
    • 此时活跃事务:仅事务 2(ID=200)自身(假设事务 3 未开始)。
    • ReadView:m_ids=[200]min_trx_id=200max_trx_id=301creator_trx_id=200
    • 检查记录版本(trx_id=100):
      100 < min_trx_id(200) → 可见,查询结果为 “张三”。
  3. 事务 3 修改记录并提交
    • 记录最新版本的DB_TRX_ID=300DB_ROLL_PTR指向旧版本(trx_id=100)。
    • 事务 3 提交后,从活跃事务集合中移除。
  4. 事务 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):符合可见规则 → 查询结果仍为 “张三”(实现可重复读)。

MVCC 的优势与局限

优势:

  1. 读写不冲突:读操作访问历史版本,无需加锁;写操作仅锁定当前版本,提高并发性能。
  2. 隔离级别支撑:通过 ReadView 的生成时机,灵活支撑 RC 和 RR 隔离级别。

局限:

  1. undo log 膨胀:版本链可能积累大量旧版本,需依赖 InnoDB 的 purge 机制清理(异步删除已无用的旧版本)。
  2. 仅适用于特定隔离级别READ UNCOMMITTED(总是读最新版本)和SERIALIZABLE(表级锁)不依赖 MVCC

欢迎关注我的其它发布渠道