Mysql Mvcc理解

MySQL 的 MVCC (Multi-Version Concurrency Control,多版本并发控制) 是 InnoDB 存储引擎实现高并发事务隔离的核心机制。它的核心目标是:让读操作不加锁,读写互不阻塞,同时保证事务的隔离性(特别是读已提交 RC 和可重复读 RR 级别)。

为了让你彻底理解,我将通过核心组件图解版本链形成过程ReadView 可见性判断 以及不同隔离级别的差异四个部分进行详细拆解。

一、MVCC 的三大核心组件(图解基础)

MVCC 的实现依赖于三个隐形或显性的组件,它们共同构成了一个"时间机器",让每个事务能看到不同时间点的数据快照。

1. 隐藏字段 (Hidden Columns)

InnoDB 会在每一行记录中自动添加三个隐藏字段(你建表时看不到,但底层一定有):

表格

字段名 含义 作用
DB_TRX_ID 最近修改该行数据的事务 ID 记录是谁最后一次改动了这行数据
DB_ROLL_PTR 回滚指针 (Rollback Pointer) 指向该行数据的上一个版本在 Undo Log 中的地址
DB_ROW_ID 隐藏行 ID 如果没有主键,用它做聚簇索引键(与 MVCC 逻辑关系不大,略过)

2. Undo Log (版本链)

当数据被修改时,旧数据不会被直接覆盖,而是被写入 Undo Log

  • 新数据写在当前行。
  • 旧数据通过 DB_ROLL_PTR 指针串联起来,形成一条链表
  • 这条链表被称为 版本链 (Version Chain)

3. ReadView (读视图)

这是 MVCC 的"灵魂"。当事务进行快照读(普通 SELECT)时,会生成一个 ReadView。

  • 它记录了当前系统中活跃事务列表(即那些已经开始但还没提交的事务 ID)。
  • 事务利用 ReadView 中的规则,去版本链上寻找"对自己可见"的那个数据版本。

二、场景演示:版本链是如何形成的?

假设有一行数据 id=1, name='A'。我们模拟三个事务的操作,看看版本链怎么变。

初始状态:

plain 复制代码
[当前行]
name: 'A'
DB_TRX_ID: 10 (创建者)
DB_ROLL_PTR: null

步骤 1:事务 T1 (ID=20) 修改数据

事务 T1 将 name 改为 'B'

  1. 把旧值 'A' 写入 Undo Log。
  2. 更新当前行:name='B', DB_TRX_ID=20
  3. DB_ROLL_PTR 指向 Undo Log 中的 'A'

此时版本链:

plain 复制代码
[当前行] (name='B', trx_id=20) 
   ⬇️ (DB_ROLL_PTR)
[Undo Log 1] (name='A', trx_id=10)

步骤 2:事务 T2 (ID=30) 修改数据

事务 T2 将 name 改为 'C'

  1. 把当前旧值 'B' 写入 Undo Log。
  2. 更新当前行:name='C', DB_TRX_ID=30
  3. DB_ROLL_PTR 指向刚才生成的 Undo Log ('B')。

此时版本链:

plain 复制代码
[当前行] (name='C', trx_id=30) 
   ⬇️ 
[Undo Log 2] (name='B', trx_id=20) 
   ⬇️ 
[Undo Log 1] (name='A', trx_id=10)

关键点:无论数据被修改多少次,所有历史版本都通过指针串在一起,最新的数据在最前面,最老的数据在最后面。

三、核心算法:ReadView 如何判断数据可见性?

当一个事务(比如事务 T4, ID=40)执行 SELECT 时,它会拿着自己的 ReadView 去遍历上面的版本链。

1. ReadView 的结构

主要包含两个关键信息(简化版):

  • m_ids : 当前活跃事务 ID 列表(即:开始但未提交的事务)。例如 [20, 30]
  • min_trx_id: 活跃事务中最小的 ID (20)。
  • max_trx_id: 生成 ReadView 时系统分配给下一个事务的 ID (假设为 40)。

2. 可见性判断规则 (核心逻辑)

对于版本链上的某一个版本,其事务 ID 为 trx_id

  1. ** **trx_id** < ****min_trx_id**
    • 说明该版本是由已经提交的老事务创建的。
    • 可见
  2. ** **trx_id** >= ****max_trx_id**
    • 说明该版本是由将来才启动的事务创建的(不可能发生,除非逻辑错误)或者是在当前事务启动后才启动的事务。
    • 不可见
  3. ** **min_trx_id** <= **trx_id** < ****max_trx_id**
    • 说明该事务在 ReadView 生成时是"活跃"的。
    • 检查 trx_id 是否在 m_ids 列表中:
      • 在列表中 :说明该事务还没提交。❌ 不可见(继续沿着指针找下一个旧版本)。
      • 不在列表中 :说明该事务在 ReadView 生成前已经提交了。✅ 可见

3. 图解查找过程

假设事务 T4 (ID=40) 发起查询,此时活跃事务列表 m_ids = [20, 30] (假设 T2 还没提交,T1 也没提交)。

  • 检查当前行 (trx_id=30)
    • 30 在 m_ids 中吗?在。
    • 结论:T2 未提交,不可见。👉 沿指针向下找。
  • 检查第二个版本 (trx_id=20)
    • 20 在 m_ids 中吗?在。
    • 结论:T1 未提交,不可见。👉 沿指针向下找。
  • 检查第三个版本 (trx_id=10)
    • 10 < min_trx_id (20)。
    • 结论:老事务已提交,可见
    • 返回结果: **name='A'**

即使当前数据库里最新的数据是 'C',只要修改它的事务没提交,或者在特定隔离级别下不符合可见性规则,你就只能看到 'A'。这就是快照读

四、关键差异:RC (读已提交) vs RR (可重复读)

MVCC 在这两个隔离级别下的行为差异,完全取决于 ReadView 生成的时机

表格

特性 读已提交 (RC) 可重复读 (RR)****(MySQL 默认)
ReadView 生成时机 每次 执行 SELECT 时都会重新生成一个新的 ReadView。 只在第一次 执行 SELECT 时生成,后续复用同一个 ReadView。
现象描述 能读到其他事务刚刚提交的最新数据。 无论其他事务提交多少次,我看到的都是事务开始时的数据快照。
解决幻读 不能解决幻读(配合间隙锁也只能部分解决)。 结合间隙锁 (Next-Key Lock),基本解决了幻读。

场景对比图解

假设数据初始为 v1

  • 事务 A (ID=100) 启动。
  • 事务 B (ID=101) 启动,修改数据为 v2 并提交。
  • 事务 C (ID=102) 启动,修改数据为 v3 并提交。

如果是 RC 级别:

  1. 事务 A 执行 SELECT -> 生成 ReadView_1 (看到 v1)。
  2. 事务 B 提交 v2
  3. 事务 A 再次 SELECT -> 生成新的 ReadView_2
    • 此时 B 已提交,不在活跃列表。
    • 结果:看到 v2(数据变了)
  4. 事务 C 提交 v3
  5. 事务 A 再次 SELECT -> 生成新的 ReadView_3
    • 结果:看到 v3(数据又变了)

如果是 RR 级别:

  1. 事务 A 执行 SELECT -> 生成 ReadView_1 (看到 v1)。【锁定快照】
  2. 事务 B 提交 v2
  3. 事务 A 再次 SELECT -> 复用 ReadView_1
    • 虽然 B 提交了,但在 ReadView_1 生成时,B 是活跃的(或未开始的,视具体时序),根据规则判定不可见(或者因为快照锁定)。
    • 结果:依然看到 v1(数据不变)
  4. 事务 C 提交 v3
  5. 事务 A 再次 SELECT -> 复用 ReadView_1
    • 结果:依然看到 v1(实现可重复读)

五、总结与注意事项

  1. 当前读 (Current Read)
    • SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, UPDATE, DELETE, INSERT
    • 这些操作不使用 MVCC,它们读取的是最新版本的数据,并且会加锁(排他锁或共享锁),确保读到的是最新的且防止并发修改。
  2. 快照读 (Snapshot Read)
    • 普通的 SELECT
    • 使用 MVCC,不加锁,效率高,读取的是历史版本。
  3. 长事务的危害
    • 如果一个长事务一直不提交,它生成的 ReadView 会一直保留,导致它需要的旧版本数据无法被 Purge 线程清理。
    • 这会导致 Undo Log 无限膨胀,占用大量磁盘空间,甚至拖慢数据库性能。
  4. 幻读问题
    • MVCC 主要解决的是行级的读写冲突和不可重复读。
    • 对于范围查询 导致的幻读(插入新行),InnoDB 在 RR 级别下还需要配合 间隙锁 (Gap Lock)临键锁 (Next-Key Lock) 来彻底解决。

通过这套机制,MySQL InnoDB 在保证数据一致性的前提下,极大地提升了并发读取的性能,实现了"读写不阻塞"。

相关推荐
CHQIUU2 小时前
PostgreSQL vs MySQL:选型指南与深度对比
数据库·mysql·postgresql
polaris06302 小时前
学生成绩管理系统(MySQL)
android·数据库·mysql
ErizJ5 小时前
面试|Mysql八股
mysql·面试
重庆小透明5 小时前
【搞定面试之mysql】第二篇:事务和MVCC
java·后端·mysql·面试·职场和发展
用户851160276125 小时前
慢 SQL 如何排查和优化?
mysql·面试
panzer_maus5 小时前
Mysql中的undo log和redo log, bin log的介绍
数据库·mysql
ssdfang5 小时前
【MySQL 的数据目录】
数据库·mysql·adb
gjc5926 小时前
【MySQL安全】密码插件指南:从配置到踩坑
数据库·mysql·安全
秦渝兴6 小时前
用 Docker Compose 一键部署高可用集群(MySQL + Tomcat + Nginx)
运维·mysql·nginx·docker·容器·tomcat