MySQL MVCC(多版本并发控制)实现机制详解

作者:技术拾光时间:2026-06-13 08:35:01浏览:418

MVCC是InnoDB通过隐藏字段、UndoLog版本链和ReadView实现的多版本并发控制技术,使快照读无需加锁,实现读不阻塞写、写不阻塞读。可重复读隔离级别下事务首次SELECT生成并复用ReadView,读已提交则每次新建。该机制提升并发性能,但版本链和UndoLog清理会带来额外开销。

前言

MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB存储引擎用来解决读写冲突、提升并发性能的核心技术。简单来说,它通过为数据行维护多个版本(快照),让普通的读操作(SELECT)无需加锁就能拿到数据,从而实现读不阻塞写、写不阻塞读的高并发效果。

MySQL MVCC(多版本并发控制)实现机制详解

一、MVCC核心概念

1.1 到底什么是MVCC

MVCC是InnoDB专门为读已提交(RC)和可重复读(RR)这两个隔离级别设计的并发控制机制。它的核心理念其实不难理解:

  • 核心思想:为每一行数据维护多个版本(快照),不同事务在读取时,根据一套规则选择对应版本的数据,而不是直接读取最新数据。
  • 适用场景:仅针对快照读(普通SELECT)生效。如果是当前读(比如SELECT … FOR UPDATE、LOCK IN SHARE MODE、以及INSERT/UPDATE/DELETE这些操作),仍然需要加锁。
  • 核心目标:解决“读写冲突”,避免传统锁机制下“读阻塞写、写阻塞读”的问题,从而大幅提升并发性能。

1.2 MVCC到底想解决什么问题

  • 读操作无锁化:快照读不需要加行锁或表锁,自然也就不会阻塞写操作。
  • 数据一致性:确保不同事务在各自的隔离级别下,读到符合规则的一致数据。
  • 历史版本可回溯:通过版本链,事务可以读到“过去某个时间点”的数据,比如RR隔离级别下的可重复读。

二、MVCC实现的三大基础组件

InnoDB的MVCC要依赖行记录隐藏字段、Undo Log(回滚日志)和Read View(读视图)这三个核心组件,它们协同工作,才完成了多版本数据的管理和读取。

2.1 行记录中的隐藏字段:你看不到但它们确实存在

InnoDB为每一行数据(除了你自定义的字段)都自动添加了3个隐藏字段,这是MVCC的基础:

隐藏字段 字段类型 核心作用
DB_TRX_ID 6字节 记录最后一次插入或更新该行数据的事务ID(删除也被视为特殊的更新,会标记一个删除标识);
DB_ROLL_PTR 7字节 回滚指针,指向该行数据的Undo Log版本链;通过这个指针,可以回溯到之前的历史版本;
DB_ROW_ID 6字节 如果聚簇索引没有指定主键或唯一键,InnoDB会自动生成一个行ID;这个字段主要是用来标识行,不是MVCC的核心;

举个例子:假设有表user(id INT PRIMARY KEY, name VARCHAR(20)),插入一行(1, '张三')。你看到的只是一条简单的记录,但实际存储结构是这样的:

id name DB_TRX_ID DB_ROLL_PTR DB_ROW_ID
1 张三 100 指向Undo Log NULL

这里的DB_TRX_ID=100表示插入这条记录的事务ID是100。

2.2 Undo Log:历史的存档室

Undo Log是InnoDB在修改数据时,记录的“数据修改前的快照”,它是实现版本链的核心载体。

2.2.1 Undo Log的两种类型

  • Insert Undo Log:只记录INSERT操作的日志,事务提交后就可以直接删除。因为INSERT的记录只对当前事务可见,其他事务压根不需要回溯它的历史版本。
  • Update Undo Log:记录UPDATE和DELETE操作的日志。事务提交后还得保留着,给其他事务的MVCC读取历史版本用。直到确认没有任何事务需要访问这个版本时,Purge线程才会过来清理。

2.2.2 版本链是怎么串起来的

每次更新一行数据时,InnoDB会按以下步骤维护版本链:

  1. 把更新前的数据写进Update Undo Log;
  2. 更新行记录中的DB_TRX_ID,改为当前事务的ID;
  3. 更新行记录中的DB_ROLL_PTR,让它指向刚才生成的Update Undo Log;
  4. 多次更新下来,DB_ROLL_PTR就会把这些历史版本像链条一样串起来,形成版本链。链头是最新版本,链尾是最早版本。

版本链长什么样?比如:当前行版本(DB_TRX_ID=102) → DB_ROLL_PTR → 版本1(DB_TRX_ID=101) → DB_ROLL_PTR → 版本0(DB_TRX_ID=100)

2.3 Read View:版本可见性的裁判

Read View是事务在执行快照读时,生成的一套“可见性判断规则”。它的核心任务很简单:判断当前事务能看到哪些版本的数据。

2.3.1 Read View里藏着哪些关键信息

Read View包含4个关键字段,用来判断版本是否可见:

字段名 含义
m_ids 生成Read View时,当前所有活跃事务的ID集合(也就是还没提交的事务ID);
min_trx_id m_ids中最小的事务ID;
max_trx_id 系统下一个要分配的事务ID,它肯定大于当前所有已分配的事务ID;
creator_trx_id 创建这个Read View的事务ID,也就是当前正在执行快照读的那个事务;

2.3.2 版本可见性是怎么判定的

事务读取行数据时,会拿着Read View去判断该行的某个版本是否可见。假设待判断版本的DB_TRX_ID = trx_id,规则如下:

  1. 如果trx_id < min_trx_id:说明这个版本是由“已经提交的事务”生成的,可见。
  2. 如果trx_id >= max_trx_id:说明这个版本是由“未来的事务”生成的(当前事务开始时,这个事务还没创建呢),所以不可见。
  3. 如果min_trx_id ≤ trx_id < max_trx_id:这种情况需要仔细看——
    • 如果trx_id ∈ m_ids:说明这个版本是由“当前活跃但还没提交的事务”生成的,不可见。
    • 如果trx_id m_ids:说明这个版本是由“已经提交的事务”生成的,可见。
  4. 如果版本不可见,那就顺着DB_ROLL_PTR回溯版本链,一直找到第一个可见版本为止;如果实在找不到,那就说明这条记录对当前事务不可见。

三、MVCC核心执行逻辑(以RR隔离级别为例)

我们拿MySQL默认的可重复读(RR)隔离级别来拆解一下,看看MVCC在INSERT、UPDATE、DELETE、SELECT这些操作中到底是怎么运行的。

3.1 插入数据(INSERT)

  1. 事务T1(ID=100)执行INSERT INTO user(id, name) VALUES (1, '张三');
  2. InnoDB给这行数据写上DB_TRX_ID=100,DB_ROLL_PTR=NULL(因为还没有历史版本);
  3. 同时生成一条Insert Undo Log,但这条日志只用来事务回滚,事务一提交就会被删除;
  4. 事务提交后,这行数据就对其他所有已提交的事务可见了。

3.2 更新数据(UPDATE)

  1. 事务T2(ID=101)执行UPDATE user SET name='李四' WHERE id=1;
  2. InnoDB先把这行数据的当前版本(DB_TRX_ID=100)保存到Update Undo Log里;
  3. 然后更新行记录:DB_TRX_ID改成101,DB_ROLL_PTR指向刚刚生成的Update Undo Log;
  4. 此时版本链长这样:当前版本(101) → 历史版本(100);
  5. 事务提交后,Update Undo Log还得留着,供其他事务读取历史版本使用。

3.3 删除数据(DELETE)

InnoDB把DELETE当成一种“特殊的UPDATE”来处理:

  1. 事务T3(ID=102)执行DELETE FROM user WHERE id=1;
  2. InnoDB同样先把这一行的当前版本保存到Update Undo Log里;
  3. 然后更新行记录:DB_TRX_ID改成102,同时标记一个“删除标识”;真正的物理删除是交给Purge线程异步完成的。
  4. 此时版本链变成:当前版本(102,标记删除) → 版本1(101) → 版本0(100)。

3.4 读取数据(SELECT,快照读)

假设当前有两个活跃事务:ID分别是101和102。事务T4(ID=103)执行SELECT * FROM user WHERE id=1(RR隔离级别):

  1. T4第一次执行SELECT时,会生成一个Read View:
    • m_ids = {101, 102},min_trx_id=101,max_trx_id=104,creator_trx_id=103。
  2. Read这行数据的当前版本(DB_TRX_ID=102),判断可见性:
    • 102 大于等于 min_trx_id(101),且小于 max_trx_id(104),并且101就在 m_ids 里,所以不可见。
  3. 通过DB_ROLL_PTR回溯版本链,拿到上一个版本(DB_TRX_ID=101):
    • 101也在 m_ids 里,仍然不可见。
  4. 继续回溯,找到版本0(DB_TRX_ID=100):
    • 100 小于 min_trx_id(101),所以可见。
  5. 最终返回这个版本的数据:name='张三'。
  6. 在RR隔离级别下,T4后续的SELECT会复用这个Read View,所以多次读取的结果完全一致——这正是“可重复读”的由来。

四、不同隔离级别下的MVCC行为

MVCC只在读已提交(RC)和可重复读(RR)这两个隔离级别下生效,它们之间的核心区别只在于Read View的创建时机:

隔离级别 Read View创建时机 读取结果特点
读已提交(RC) 每次执行快照读(SELECT)时,都重新创建一个Read View 同一个事务内,多次SELECT可能读到不同的版本(不可重复读),因为只能看到已提交的最新版本;
可重复读(RR) 事务内第一次执行快照读时创建Read View,后续的SELECT都复用这个视图 同一个事务内,多次SELECT结果一致(可重复读),因为只能看到事务启动时就已经提交的版本;
读未提交(RU) 不使用MVCC,直接读取最新数据 能看到未提交事务的数据,所以存在脏读问题;
串行化(SERIALIZABLE) 禁用MVCC,所有读操作都加表锁 完全串行执行,没有并发冲突,但性能极低;

五、MVCC的优势与局限

5.1 优势在哪里

  • 高并发:快照读不需要加锁,读写互不阻塞,并发性能大幅提升。
  • 数据一致性:在RC和RR隔离级别下,能保证读取数据的一致性,避免脏读(RC和RR都行),还能避免不可重复读(RR)。
  • 无锁读:不用加行锁或表锁,锁竞争和死锁的概率自然就降低了。

5.2 缺陷与代价

  • 版本链开销:大量更新操作会导致版本链变得越来越长,读取时的回溯成本也会随之增加。
  • Undo Log清理压力:Purge线程需要异步清理过期的Undo Log,如果清理不及时,会占用大量磁盘空间。
  • 仅适用于快照读:当前读(比如SELECT … FOR UPDATE)仍然需要加锁,所以写冲突的隐患并没有完全消除。

总结

  1. 核心依赖:MVCC靠的是行记录隐藏字段(DB_TRX_ID和DB_ROLL_PTR)、Undo Log版本链以及Read View可见性规则来工作。
  2. 核心逻辑:更新数据时生成版本链,读取数据时通过Read View判断版本是否可见,再回溯版本链找到符合规则的版本。
  3. 隔离级别差异:RC隔离级别下,每次SELECT都会新建一个Read View,所以可能出现不可重复读;RR隔离级别则只在第一次SELECT时创建Read View,后续一直复用,从而实现可重复读。
  4. 核心价值:一句话总结,MVCC让“读不阻塞写、写不阻塞读”成为现实,这正是InnoDB实现高并发的根基。