当前位置: 首页 > backend >正文

MySQL-MVCC多版本并发控制详解

一、MVCC 是什么?

MVCC,全称 Multi-Version Concurrency Control,即多版本并发控制

它是一种为了提高数据库并发性能而提出的技术方案。其核心思想是:
为每一行数据维护多个历史版本(通常是多个快照),使得在事务并发执行时,读操作不会阻塞写操作,写操作也不会阻塞读操作。 从而极大提高了数据库的读性能,并且避免了常见的锁等待。

MySQL 中,只有 InnoDB 存储引擎支持 MVCC


二、为什么需要 MVCC?—— 解决锁的弊端

在没有 MVCC 的情况下,为了保证并发事务的隔离性(特别是 REPEATABLE-READ 和 READ-COMMITTED 级别),通常需要加锁。

  • 写操作(如 UPDATEDELETE)会给行加排他锁(X Lock),这会阻塞其他事务的读和写。

  • 读操作(如 SELECT)为了确保读取一致的数据,可能也需要加共享锁(S Lock),这会阻塞其他事务的写。

这种“读-写”或“写-读”操作的相互阻塞,在高并发场景下会严重降低数据库的性能。

MVCC 提供了一种非锁读的机制,通过快照的方式让读操作可以去读历史版本的数据,而写操作可以同时更新当前的最新版本,二者互不干扰,完美解决了上述问题。


三、MVCC 的实现细节

MVCC 在 InnoDB 中的实现依赖于三个核心组件和 Undo Log

1. 隐藏字段

InnoDB 为每一行数据(聚簇索引记录)添加了两个(或三个)隐藏的系统字段:

  • DB_TRX_ID (6字节):最后修改该行数据的事务ID。当一个事务插入或修改某行时,会把自己的 transaction id 写入这个字段。

  • DB_ROLL_PTR (7字节):回滚指针。指向该行数据在 undo log 中的上一个历史版本。所有的历史版本通过这个指针串联成一个版本链

  • DB_ROW_ID (6字节):行ID(隐藏主键)。如果表没有显式定义主键,InnoDB 会自动生成这个字段作为聚簇索引的主键。

2. Undo Log (回滚日志)

Undo Log 是 MVCC 的基石。它主要分为两种:

  • insert undo log:记录插入操作产生的日志,只在事务回滚时需要,事务提交后即可丢弃。

  • update undo log:记录更新和删除操作产生的日志。它才是 MVCC 实现的关键

当执行 UPDATE 或 DELETE 操作时:

  • 并不是直接物理删除当前数据,而是将当前行数据拷贝一份到 Undo Log 中

  • 然后用新的数据更新当前行,并更新 DB_TRX_ID 和 DB_ROLL_PTR 字段,让 DB_ROLL_PTR 指向刚刚存入 Undo Log 的旧版本记录。

  • 旧版本记录中的 DB_ROLL_PTR 又指向前一个更旧的版本。这样就形成了一条版本链。链首是最新的记录,链尾是最旧的记录。

3. Read View (读视图)

Read View 是事务进行快照读操作时产生的。 它定义了当前事务能看到哪个版本的数据。

当一个事务执行 SELECT 语句(快照读)时,InnoDB 会为该事务生成一个 Read View。它主要包含以下内容:

  • m_ids:生成 Read View 时,系统中所有活跃(未提交)的事务ID列表

  • min_trx_id:生成 Read View 时,系统中活跃事务中最小的事务ID,也就是 m_ids 中的最小值。

  • max_trx_id:生成 Read View 时,系统应该分配给下一个事务的ID(即当前最大事务ID + 1)。

  • creator_trx_id创建该 Read View 的事务的ID(只有执行写操作的事务才会被分配ID,只读事务的ID默认为0)。


四、可见性算法:如何判断版本是否可见?

有了版本链和 Read View,当一个事务要读取某行数据时,InnoDB 会从最新的版本开始,顺着版本链依次检查每个版本,根据 Read View 中的信息判断该版本是否对当前事务可见

判断规则如下(假设要判断版本链中的某个记录 R):

  1. 如果 R 的 DB_TRX_ID == creator_trx_id,说明这个版本是当前事务自己修改的,可见

  2. 如果 R 的 DB_TRX_ID < min_trx_id,说明这个版本在当前 Read View 创建之前就已经提交了,可见

  3. 如果 R 的 DB_TRX_ID >= max_trx_id,说明这个版本是在当前 Read View 创建之后才开启的事务修改的,肯定不可见

  4. 如果 R 的 DB_TRX_ID 在 min_trx_id 和 max_trx_id 之间(即 trx_id in m_ids?):

    • 如果 DB_TRX_ID 在 m_ids(活跃事务列表)中,说明修改该版本的事务在生成 Read View 时还未提交,该版本不可见

    • 如果 DB_TRX_ID 不在 m_ids 中,说明修改该版本的事务在生成 Read View 时已经提交,该版本可见

如果某个版本对当前事务不可见,就顺着版本链找到上一个版本,重复上述判断规则,直到找到第一个可见的版本或到达链尾。

不同隔离级别下 Read View 的生成时机不同,这导致了可见性的差异:

  • READ COMMITTED每次执行快照读时都会生成一个新的 Read View。所以每次都能看到最新提交的数据。

  • REPEATABLE READ:只在第一次执行快照读时生成一个 Read View,后续所有读操作都复用这个相同的 View。所以整个事务期间看到的数据都是一致的。


五、举例说明

假设有表 t,初始数据:

idvalue
1A

事务时序:

  1. 事务Trx10BEGIN; UPDATE t SET value = ‘B’ WHERE id = 1; (未提交)

  2. 事务Trx20BEGIN; UPDATE t SET value = ‘C’ WHERE id = 1; COMMIT;

  3. 事务Trx30BEGIN; 然后执行 SELECT * FROM t WHERE id = 1;

此时,版本链为:当前行 (value=‘C', trx_id=20, roll_ptr) --> Undo Log (value=‘B', trx_id=10, roll_ptr) --> Undo Log (value=‘A', trx_id=初始, roll_ptr=NULL)

情况1:隔离级别为 READ COMMITTED
Trx30 执行 SELECT生成新的 Read View

  • m_ids: [10, 30] (假设Trx10未提交,Trx30自己是活跃的)

  • min_trx_id: 10

  • max_trx_id: 31 (下一个事务ID)

  • creator_trx_id: 30

判断过程:

  1. 最新版本 value=‘C'trx_id=20

    • 20 < min_trx_id(10)? 不成立。

    • 20 在 m_ids 中吗?不在。所以此版本对 Trx30 可见

    • 结果:读取到 ‘C'

情况2:隔离级别为 REPEATABLE READ
Trx30 在事务中第一次执行 SELECT,生成 Read View:

  • m_ids: [10, 30]

  • min_trx_id: 10

  • max_trx_id: 31

  • creator_trx_id: 30

判断过程:

  1. 最新版本 value=‘C'trx_id=20

    • 20 不在 m_ids 中,且 20 > min_trx_id(10),且 20 < max_trx_id(31)。根据规则4,可见

    • 注意规则4的细节trx_id(20) 在 min 和 max 之间,需要检查是否在 m_ids 中。因为 m_ids 是 [10,30],20 不在其中,说明 Trx20 在生成 Read View 时已经提交了?等一下!这里有个关键点:Trx20 的提交是在 Trx30 生成 Read View 之后 还是 之前

实际上,在 REPEATABLE READ 下,Trx30 的 Read View 在第一次 SELECT 时就生成了。假设 Trx20 的提交发生在 Trx30 的 Read View 生成之后,那么 Trx20 的 ID(20) 对于 Trx30 的 Read View 来说,是大于等于 max_trx_id(31) 的吗?不,这里需要更精确的理解。

一个更准确的例子是,如果 Trx20 在 Trx30 开始之前就提交了,那么它的 trx_id(20) 会小于 Trx30 的 min_trx_id(30的View中的min是10,但20大于10),并且不在活跃事务列表中,因此是可见的。但如果 Trx20 在 Trx30 开始之后才提交,那么它在 Trx30 的 View 中就是不可见的。

为了避免混淆,记住核心:RR 级别下,一个事务只使用它开始时那个瞬间的快照。所有在该瞬间之后提交的事务所做的修改,对它都是不可见的。所以 Trx30 只能看到在它开始之前已经提交的数据(Trx20如果后提交,则不可见),或者它自己修改的数据。


六、Purge(清理)操作

Undo Log 中的历史版本不会永远保留。当系统里没有任何一个 Read View 需要用到某个旧版本时(即没有任何事务还需要看到这个版本),这个 Undo Log 记录就可以被清除了。InnoDB 有后台的 Purge 线程来负责这项工作。

这就是为什么强烈不建议使用长事务的原因。一个很长的事务会一直维持一个很老的 Read View,导致大量的旧版本数据无法被 Purge,从而使得 Undo Log 不断膨胀,占用大量存储空间。


总结

特性说明
目标实现无锁的非阻塞读,提高并发性能。
实现基础隐藏字段 (DB_TRX_IDDB_ROLL_PTR) + Undo Log(形成版本链) + Read View(判断可见性)。
与隔离级别的关系RC:每次读生成新ReadView,能看到最新提交。
RR:第一次读生成ReadView并复用,保证可重复读。
优点读不上锁,读写不冲突,并发性能极高。
缺点需要维护多版本数据,带来了额外的存储空间(Undo Log)和维护开销。
http://www.xdnf.cn/news/19798.html

相关文章:

  • LangChain实战(十二):自定义Tools扩展Agent能力
  • Python+DRVT 从外部调用 Revit:批量创建门
  • Streamable HTTP
  • sv中forever如何结束
  • AI 在金融、医疗、教育、制造业等领域有着广泛的应用,以下是这些领域的一些落地案例
  • STM32HAL 快速入门(十七):UART 硬件结构 —— 从寄存器到数据收发流程
  • 告别剪辑烦恼!3个超实用技巧,让你的视频瞬间高级起来
  • 【音视频】视频秒播优化实践
  • UnityWebRequest 数据获取和提交
  • wpf 只能输入int类型的文本框
  • WebSocket客户端库:websocket-fruge365
  • Ubuntu下把 SD 卡格式化为 FAT32
  • Hostol Magento电商服务器套餐:基于阿里云,预配置高性能环境,一键开店
  • 如何用java给局域网的电脑发送开机数据包
  • B样条曲线,已知曲线上的某个点到起点的距离,确定这个点的参数u的值的方法
  • 新手向:破解VMware迁移难题
  • MP4视频太大如何压缩?分享6种简单便捷的压缩小技巧
  • websocket用于控制在当前页只允许一个用户进行操作,其他用户等待
  • 硬件(一)51单片机
  • 阿里开源首个图像生成基础模型——Qwen-Image本地部署教程,中文渲染能力刷新SOTA
  • HTTP 协议核心组件与安全扩展深度解析
  • 机器学习与深度学习的 Python 基础之 NumPy(2)
  • uniapp+vue3 微信小程序全屏广告组件功能
  • AI IDE+AI 辅助编程,真能让程序员 “告别 996” 吗?
  • 【LeetCode_283】移动零
  • 技术小白如何快速的了解opentenbase?--把握四大特色
  • XE 旧版本 JSON 处理
  • 使用 Uni-app 打包 外链地址APK 及 iOS 注意事项
  • K8S-基础架构
  • 离开职场2个月,后知后觉的反思。