MySQL——MVCC

1.为什么需要MVCC

在并发场景下,读写操作会面临严重的冲突问题:

1.读操作如果遇到写操作,要么"读到未提交的脏数据",要么"被写操作阻塞(等待锁释放)";

2.写操作如果遇到读操作,要么"覆盖读操作需要的数据",要么"被读操作阻塞";

MVCC通过"多版本"解决了这个问题:写操作会生成新的数据版本,读操作读取旧的版本(不影响写),两者不干扰。

2.什么是MVCC

MySQL的MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB存储引擎实现高并发读写的核心操作。核心思想:为数据库中的每条记录维护多个版本,通过"版本链"和"可间性判断规则",让不同实物在并发访问时,能够看到符合自己隔离级别的数据版本,从而避免读写冲突(读不阻塞写,写不阻塞读),同事保证事务隔离性。

3.MVCC的核心组成

MVCC的实现依赖三个关键组件:隐藏列(版本标识)Undo Log(版本存储)Read View(可见性判断)

3.1 隐藏列:记录的"版本身份证"

InnoDB为表中的每条记录添加了三个隐藏字段,用于记录标识的版本信息

  • DB_TRX_ID:记录最后一次修改该记录的事务ID(6字节)。每次事务对记录执行insert/update/delete时,都会将自己的事务ID写入该字段。
  • DB_ROLL_PTR:回滚指针(7字节)。指向该记录的"上个版本"在Undo Log中的位置(通过它可以串联所有的历史版本,形成"版本链")。
  • DB_ROW_ID:记录的唯一标识(6字节)。如果表没有定义主键或唯一索引,InnoDB会用它作为默认聚簇索引(一般用不到,可忽略)。

3.2 Undo Log:版本的"历史记录馆"

Undo Log(回滚日志)是InnoDB用于存储"记录旧版本"的空间。当视为修改记录时,旧版本的数据 不会被直接删除,而是被写入Undo Log,供后序"回滚"或"其他事务读取"使用。

与隐藏列相结合形成"版本链",记录的"版本链"形成的过程如下:

  1. 初始插入一条数据时,DB_TRX_ID是插入事务的ID,DB_ROLL_PTR为null(无历史版本,新数据无历史版本)。
  2. 当事务A(事务ID=110)修改该记录时,InnoDB会先将旧版本数据写入Undo Log,然后更新记录的DB_TRX_ID=110,并将DB_ROLL_PTR指向Undo Log中的旧版本。
  3. 之后事务B(事务ID=220)再次修改该记录时,会将"事务A修改后的版本"写入Undo Log,更新DB_TRX_ID=220,DB_ROLL_PTR指向事务A版本再Undo Log的位置。
  4. 最终,通过DB_ROLL_PTR串联的Undo Log记录,形成了改记录的"版本链"(最新版本在表中,历史版本在Undo Log中)。

一条记录的版本链结构(简化):

当前记录(表中):

data:(name='HajiHang',age=21)|DB_TRX_ID=220|DB_ROLL_PTR------>Undo Log中的版本1

Undo Log:

Undo Log中的版本1(事务A修改后的版本):

data:(name='HajiHang',age=20)|DB_TRX_ID=110|DB_ROLL_PTR------>Undo Log中的版本0

Undo Log中的版本0(初始插入版本):

data:(name='HajiHang',age=19)|DB_TRX_ID=55|DB_ROLL_PTR------>null

3.3 Read View:版本的"可见性过滤器"

有了版本链后,事务 如何判断"哪个版本的数据对自己可见"?这就需要Read View(读试图),它是一个"可见性判断规则集合",用于确定当前事务能够看到版本链中的那个版本。

Read View包含4个核心变量(生成时确定):

  1. m_ids:生成Read View时,当前所有"活跃事务"(已启动但未提交)的事务ID集合(无序)
  2. min_trx_id:m_ids中的最小事务ID(当前活跃事务中最早启动的那个)
  3. max_trx_id:生成Read View时,"下一个将要分配的事务ID"(并非m_ids中的最大值,而是一个预分配的自增ID)
  4. creator_trx_id:当前生成Read View的事务自己的ID

4.可见性判断规则(核心)

设记录的版本对应的事务ID为trx_id,根据trx_id和Read View的变量对比:

  • 如果trx_id == creator_trx_id:该版本是当前事务自己修改的,可见(自己改的自己当然可以看到)
  • 如果trx_id < min_trx_id:该版本对应的事务在"当前Read View生成前"就已提交(因为它的ID比所有活跃事务的最小ID还小),可见。
  • 如果trx_id >= max_trx_id:该版本对应的事务在"当前Read View生成后"才启动(ID超过预分配的最大ID),不可见(还没提交,或刚启动)。
  • 如果min_trx_id <=trx_id <=max_trx_id:
    • 若trx_id在m_ids(活跃事务集合)中:说明该事务在Read View生成时还没提交,不可见;
    • 若trx_id不在m_ids中:说明该事务在Read View生成前已提交,可见;

5.MVCC与隔离级别的关系

MVCC的核心作用是实现隔离级别(ACID中的"I"隔离性)。InnoDB通过"Read View的生成时机"和"版本链",在不同隔离级别下表现出不同的行为:

|------------------------|-------------------------------|----------------------------------------------------|
| 隔离级别 | MVCC行为(Read View生成时机) | 解决的问题 |
| Read Uncommitted(读未提交) | 不使用MVCC(直接读取最新的版本) | 可能读到未提交的脏数据(脏读) |
| Read Committed(读已提交) | 每次执行select时,重新生成Read View | 避免脏读(只看到已提交的版本);但是可能出现"不可重复读"(两次查询Read View不同) |
| Repeatable Read(可重复读) | 仅在事务第一次select生成Read View,之后复用 | 避免不可重复读(两次查询用同一个Read View,版本一致);但可能出现"幻读"(通过间隙锁解决) |
| Serializable(串行化) | 不依赖MVCC,直接通过加锁(读加共享锁,写加排它锁)实现 | 完全串行化,无并发问题,性能低 |

6.补充:不同隔离级别涉及到的并发问题

不同隔离级别会产生不同的并发问题,核心问题包括脏读,不可重复读,幻读。

  • 脏读指的是一个事务读取到另一个为提交事务修改的数据。(事务A在修改name并未提交,事务B读取到了name,后事务A又进行修改并提交/或直接回滚,导入事务B读取到了一个临时,无效的数据)
  • 不可重复读指的是同一事务内,多次读取同一数据,结果因其他已提交事务的修改而不同。(事务A第一次读取name='HajiHang',事务B执行了修改操作将name改成了Hajimi,事务A再次读取name发现不是HajiHang了)
  • 幻读指的是同一事务内,多次执行相同的查询操作(通常是范围查询)时,结果集行数因其他一提交事务的插入/删除而变化(事务A查询score=90的人发现有5条数据,之后事务B插入一个90的数据,事务A再次查询score=90发现结果集变为6条)
相关推荐
程序猿小D1 分钟前
Java项目:基于SSM框架实现的校园活动资讯网管理系统【ssm+B/S架构+源码+数据库+毕业论文+远程部署】
java·数据库·mysql·spring·毕业设计·ssm框架·校园活动
数据要素X9 分钟前
【数据架构08】数字化转型架构篇
大数据·数据库·数据仓库·架构·数据库架构
老纪的技术唠嗑局43 分钟前
Agentic AI, 基于 Dify x OceanBase 的实践
数据库
冒泡的肥皂2 小时前
数据库最近学到的小知识(一
数据库·后端·架构
wuxuanok3 小时前
SQL理解——INNER JOIN
数据库·sql
GreatSQL3 小时前
工具分享-通过开源工具 tuning-primer快速巡检MySQL5.7
数据库
天天讯通3 小时前
机器人系统对接线索平台好处
大数据·数据库·人工智能·机器人·语音识别
__風__4 小时前
从本地 Docker 部署的 Dify 中导出知识库内容(1.6版本亲测有效)
人工智能·python·mysql·语言模型
运维小杨4 小时前
Redis主从复制搭建
数据库·redis·缓存