MySQL架构揭秘:MVCC解决读一致性问题

在数据库系统中,并发控制是一个经典且复杂的话题。当多个事务同时对同一份数据进行读写操作时,如何保证数据的一致性、隔离性,同时又能兼顾高性能?MySQL InnoDB 引擎给出的答案之一就是 MVCC(Multi-Version Concurrency Control,多版本并发控制)

本文将从为什么需要 MVCCMVCC 核心原理隐藏字段与版本链一致性视图(Read View) 以及可见性判断规则五个方面,由浅入深地为你彻底讲透 MVCC。


一、为什么需要 MVCC?

想象一个场景:

  • 事务 A 正在读取用户表中的数据(耗时较长的统计查询)。

  • 与此同时,事务 B 正在修改同一条数据(比如更新余额)。

如果使用传统的加锁并发控制(如两阶段锁),那么事务 A 在读取时,事务 B 必须等待,反之亦然。这会导致数据库的并发能力严重下降,响应变慢。

MVCC 的思想是:读不阻塞写,写不阻塞读。

它通过保存数据在某个时间点的快照,让读操作可以看到该时间点之前已提交的数据版本,而不受后续写操作的影响。这样一来,读写事务之间无需互斥等待,极大地提升了并发性能。


二、MVCC 的核心概念

1. 什么是 MVCC?

MVCC(Multi-Version Concurrency Control)是一种并发控制方法,它允许数据库在读写并发时,为每个读操作提供一个一致性快照,从而实现非锁定读取。

MVCC 仅在 Read Committed(已提交读)Repeatable Read(可重复读) 两种隔离级别下生效,因为这两种级别需要"读到的数据是一致的"。而在 Read Uncommitted 下,不需要一致性视图;在 Serializable 下,则通过加锁实现完全隔离。

2. 核心思想:保留多个版本

传统更新是"覆盖旧数据",而 MVCC 的更新是**"保留旧版本,新增新版本"**:

  • 当更新一条记录时,InnoDB 不会直接覆盖原数据。

  • 而是将旧数据保留在 undo log 中,并生成一条新版本的数据。

  • 不同事务根据自身的事务 ID 和创建时间,决定能看到哪个版本。


三、InnoDB 中的隐藏字段与版本链

为了实现 MVCC,InnoDB 在每行记录中悄悄添加了三个隐藏字段(你平时用 SELECT * 是看不到的):

隐藏字段 含义
DB_TRX_ID 最近一次插入或更新该行的事务 ID(事务版本号)
DB_ROLL_PTR 回滚指针,指向该行旧版本的 undo log 记录
DB_ROW_ID 行 ID(如果表没有主键,InnoDB 会用该字段生成聚簇索引)

版本链(Version Chain)

每次更新操作,InnoDB 会:

  1. 将当前数据行的旧值复制到 undo log 中。

  2. 更新原数据行的 DB_TRX_ID 为当前事务 ID。

  3. DB_ROLL_PTR 指向刚写入的 undo log 记录。

这样一来,同一行数据的不同版本通过 DB_ROLL_PTR 串联成一条版本链,从最新版本一直回溯到最旧的版本。

复制代码
最新版本 (DB_TRX_ID = 事务5)
    ↑
    │ (DB_ROLL_PTR)
    ↑
中间版本 (DB_TRX_ID = 事务3)
    ↑
    │ (DB_ROLL_PTR)
    ↑
最旧版本 (DB_TRX_ID = 事务1)

四、一致性视图(Read View)------ 快照的核心

当执行一个查询语句(SELECT)时,InnoDB 会为该查询生成一个 Read View(一致性视图)。这个视图决定了当前查询能看到哪些版本的数据。

Read View 的核心组成包括:

  • m_ids :当前系统中所有未提交的事务 ID 数组(不包括当前事务自己)。

  • min_trx_id:m_ids 中的最小值(即最早未提交事务的 ID)。

  • max_trx_id :系统下一个将要分配的事务 ID(即当前已创建的最大事务 ID + 1)。

注意:max_trx_id 并不是当前已提交的最大事务 ID,而是"下一个即将使用的 ID",因此它一定大于所有已存在的事务 ID。


五、可见性判断规则(核心中的核心)

有了 Read View 之后,查询会沿着版本链从最新版本开始,逐个往前比对,找到第一个可见的版本

比对规则如下(我们把当前要判断的版本的事务 ID 记为 trx_id):

条件 结论 解释
trx_id < min_trx_id 可见 该版本是在所有未提交事务之前就已经提交的,是稳定的历史数据。
trx_id >= max_trx_id 不可见 该版本是由未来才启动的事务产生的(当前查询发生时它还不存在),肯定不可见。
min_trx_id <= trx_id < max_trx_id 需要进一步判断 该版本可能来自已提交或未提交的事务,需要查 m_ids。
trx_id 在 m_ids 中 不可见(特殊情况下当前事务自己的更新可见) 该版本来自一个尚未提交的事务,对其他事务来说不应看到。
trx_id 不在 m_ids 中 可见 该版本来自一个已经提交的事务,可以安全读取。

补充说明(非常重要):

  • 当前事务自己修改过的数据 ,即使 trx_id 在 m_ids 中(因为自己还未提交),但对自己的查询是可见的。这是出于事务内部一致性的需要。

  • 对于 删除操作 ,InnoDB 将其视为一种特殊的更新:它会复制最新版本的数据,将 DB_TRX_ID 改为删除事务的 ID,并在记录头信息中将 deleted_flag 标记为 true。查询时如果命中了被标记删除的记录,则视为不可见,不返回该行。


六、图解 MVCC 可见性判断

为了方便理解,我们用一张图来总结(颜色代表不同类型的事务):


七、RR 与 RC 下 MVCC 的区别

隔离级别 Read View 生成时机 效果
Read Committed 每次执行 SELECT 语句时都重新生成一个新的 Read View 能看到其他事务已提交的最新更改,但同一事务内多次读取可能不一致(不可重复读)。
Repeatable Read 事务中第一次 执行 SELECT 时生成 Read View,之后复用 整个事务期间看到的数据快照一致,保证可重复读。

这也解释了为什么 RR 级别能避免不可重复读和幻读(结合间隙锁),而 RC 级别不能。


八、MVCC 的优势与局限

优势

  • 读操作无锁 :普通 SELECT 不需要加任何锁,读写互不阻塞,高并发下性能极佳。

  • 一致性保证:提供基于时间点的数据快照,满足事务隔离性要求。

  • 回滚支持:undo log 中的版本链本身也是事务回滚的数据基础。

局限

  • 存储开销:每个更新都会产生 undo log 版本,如果事务长期不提交或并发极高,undo log 会膨胀,占用大量存储空间。

  • 清理负担:后台的 purge 线程需要定期清理那些所有事务都不再需要的旧版本数据,会增加系统负载。


九、总结

MVCC 是 MySQL InnoDB 引擎实现高并发、高一致性读的基石。它通过:

  1. 隐藏字段(DB_TRX_ID + DB_ROLL_PTR) 记录版本信息;

  2. undo log 版本链 保存历史数据;

  3. 一致性视图(Read View) 在查询时动态判断可见性;

  4. 可见性算法 精确控制每个事务能看到的数据版本。

相关推荐
张忠琳2 小时前
【kubernetes】Node Feature Discovery 0.20.0-devel 源码深度解析 — 第四卷:CLI入口、工具链与全局架构总结
云原生·容器·架构·kubernetes·nvidia
不好听6132 小时前
BFF 架构实战:从前端直调 API 到加入中间层
前端·架构
Navicat中国2 小时前
开发者入门:无服务器数据库
数据库·云原生·serverless·navicat
whn19774 小时前
如何避免以root用户执行达梦
数据库
brave_zhao4 小时前
openEuler下安装tar包解压工具
linux·服务器·数据库
Georgeviewer5 小时前
数字化工程落地复盘:通用SaaS架构的本地化缺陷与自研系统最优解|皖禾数智实战
大数据·人工智能·架构
柒和远方5 小时前
V043:BFF 层流式输出:从前端直连到生产级流式网关的架构演进
javascript·架构·node.js
fu15935745686 小时前
【边缘计算实战】P1:从零搭建边云任务卸载仿真实验台(Python 可复现)
数据库·python·边缘计算
隔壁阿布都6 小时前
MySQL 间隙锁死锁问题处理
数据库·mysql