InnoDB对于MVCC的实现

多版本并发控制(MVCC,Multi-Version Concurrency Control)是 InnoDB 实现高并发的核心机制,其核心目标是在不加锁(或低锁)的前提下,让不同事务看到符合隔离级别的数据版本,既保证读 - 写、写 - 读并发不阻塞,又能解决脏读、不可重复读、幻读等一致性问题

一、核心概念:一致性非锁定读 vs 锁定读

在讲解 MVCC 实现前,需先明确 InnoDB 的两种读模式,这是 MVCC 落地的基础:

1. 一致性非锁定读(Consistent Non-Locking Read)

  • 定义 :InnoDB 默认的读方式,读取数据时不加锁,而是通过读取数据的历史版本(快照)来保证一致性,因此不会阻塞写操作,写操作也不会阻塞该读操作。
  • 适用场景:普通 SELECT 语句(无 FOR UPDATE/LOCK IN SHARE MODE)。
  • 核心依赖:MVCC 的快照机制(ReadView + undo-log)。

2. 锁定读(Locking Read)

  • 定义 :读取数据时会对目标行加锁,强制后续写操作等待,保证读 - 写的强一致性,不依赖 MVCC 的快照
  • 适用场景 :需要保证读取的数据后续能安全修改的场景,如:
    • SELECT ... FOR UPDATE:加排他锁(X 锁),阻止其他事务修改或加共享锁;
    • SELECT ... LOCK IN SHARE MODE:加共享锁(S 锁),阻止其他事务修改,但允许加共享锁。
  • 核心依赖:InnoDB 的行锁机制(Record Lock)、间隙锁(Gap Lock)、Next-key Lock。

二、InnoDB 实现 MVCC 的核心组件

MVCC 的本质是为每行数据维护多个版本,事务根据隔离级别读取 "可见的版本",其实现依赖三大核心组件:隐藏字段undo-logReadView

1. 隐藏字段(Hidden Columns)

InnoDB 会为每张表的每行数据(除主键外)自动添加 3 个隐藏字段,用于标记数据版本和归属事务:

隐藏字段 数据类型 作用说明
DB_TRX_ID 6 字节 记录最后一次修改该行数据的事务 ID(插入 / 更新 / 删除均算修改)。
DB_ROLL_PTR 7 字节 回滚指针,指向该行数据的上一个版本(存储在 undo-log 中),形成版本链。
DB_ROW_ID 6 字节 可选字段,仅当表无主键 / 唯一非空键时生成,作为行的唯一标识(类似自增 ID)。

示例 :假设表user有显式字段id (PK)name,则实际存储结构为:

DB_ROW_ID id name DB_TRX_ID DB_ROLL_PTR
1 1 张三 100 指向 undo-log 版本 1

当事务 ID=200 的事务更新name为 "李四" 时,InnoDB 不会直接修改原行,而是:

  1. 保留原行,DB_TRX_ID仍为 100;
  2. 新增一行新版本数据,DB_TRX_ID=200DB_ROLL_PTR指向原行(版本 1);
  3. 原行的DB_ROLL_PTR无变化(成为版本链的尾节点)。

2. Undo Log(回滚日志)

Undo Log 是 InnoDB 的事务日志之一,核心作用:

  • 事务回滚:若事务执行失败,通过 undo-log 恢复数据到修改前状态;
  • MVCC 版本链 :存储数据的历史版本,与DB_ROLL_PTR配合形成 "版本链"。

(1)Undo Log 的类型

  • INSERT Undo Log:仅用于 INSERT 操作,事务提交后可直接删除(因为 INSERT 的行仅当前事务可见,无历史版本共享);
  • UPDATE/DELETE Undo Log:用于 UPDATE/DELETE 操作,事务提交后不能立即删除,需保留至所有依赖该版本的事务都已提交(通过 purge 线程清理过期版本)。

(2)版本链的形成

每行数据的多个版本通过DB_ROLL_PTR串联,最新版本在链头,最旧版本在链尾。例如:

复制代码
最新版本(trx_id=300) → DB_ROLL_PTR → 版本2(trx_id=200) → DB_ROLL_PTR → 版本1(trx_id=100) → null

3. ReadView(读视图)

ReadView 是事务执行一致性非锁定读时生成的 "可见性判断快照",本质是一个数据结构,记录了当前系统中 "活跃的未提交事务 ID",用于判断版本链中的哪个版本对当前事务可见。

ReadView 包含的核心字段

字段 作用说明
m_ids 当前系统中活跃的未提交事务 ID 列表(升序排列)。
min_trx_id m_ids中的最小值(最小活跃事务 ID)。
max_trx_id 系统下一个要分配的事务 ID(大于所有已分配的事务 ID)。
creator_trx_id 生成该 ReadView 的事务 ID(当前事务 ID)。

数据可见性算法(核心规则)

对于版本链中的某一行版本(记其DB_TRX_IDtrx_id),InnoDB 通过以下规则判断是否对当前事务可见:

  1. trx_id == creator_trx_id:该版本是当前事务自己修改的,可见;
  2. trx_id < min_trx_id:该版本由已提交的事务生成(事务 ID 小于最小活跃 ID,说明已提交),可见;
  3. trx_id >= max_trx_id:该版本由未来的事务生成(事务 ID 未分配),不可见;
  4. min_trx_id ≤ trx_id < max_trx_id
    • trx_idm_ids中(该事务仍活跃未提交),不可见;
    • trx_id不在m_ids中(该事务已提交),可见。

示例 :假设 ReadView 的min_trx_id=200max_trx_id=500m_ids=[200,300,400]creator_trx_id=150

  • 版本trx_id=100< min_trx_id,可见;
  • 版本trx_id=200:在m_ids中,不可见;
  • 版本trx_id=450:不在m_ids< max_trx_id,可见;
  • 版本trx_id=500>= max_trx_id,不可见;
  • 版本trx_id=150:等于creator_trx_id,可见。

三、RC 和 RR 隔离级别下 MVCC 的核心差异

InnoDB 的隔离级别(读未提交 RU、读已提交 RC、可重复读 RR、串行化 SERIALIZABLE)中,RU 不使用 MVCC(直接读最新数据),SERIALIZABLE 使用锁定读,只有 RC 和 RR 依赖 MVCC,核心差异在于ReadView 的生成时机

1. 读已提交(RC,Read Committed)

  • ReadView 生成时机每次执行 SELECT 语句时都生成新的 ReadView
  • 核心表现
    • 解决脏读:只能读取已提交事务的版本;
    • 无法解决不可重复读:同一事务内多次 SELECT,每次生成新的 ReadView,可能读取到其他事务提交的新版本;
    • 幻读未解决:多次 SELECT 可能看到新插入的行(因为每次 ReadView 不同)。

RC 下 MVCC 示例(不可重复读场景)

时间 事务 A(RC 隔离级) 事务 B
T1 BEGIN;
T2 SELECT name FROM user WHERE id=1; → 张三(生成 ReadView1:m_ids=[A 的 ID])
T3 BEGIN; UPDATE user SET name=' 李四 ' WHERE id=1; COMMIT;(trx_id=B 的 ID)
T4 SELECT name FROM user WHERE id=1; → 李四(生成 ReadView2:m_ids=[A 的 ID],B 的 ID 已提交,可见)

2. 可重复读(RR,Repeatable Read)

  • ReadView 生成时机仅在事务中第一次执行 SELECT(一致性非锁定读)时生成 ReadView,后续所有 SELECT 复用该 ReadView
  • 核心表现
    • 解决脏读、不可重复读:同一事务内多次 SELECT 复用同一个 ReadView,只能看到第一次 SELECT 时已提交的版本;
    • 基础 MVCC 无法完全解决幻读,需结合 Next-key Lock。
RR 下 MVCC 示例(解决不可重复读)
时间 事务 A(RR 隔离级) 事务 B
T1 BEGIN;
T2 SELECT name FROM user WHERE id=1; → 张三(生成 ReadView1:m_ids=[A 的 ID],复用至事务结束)
T3 BEGIN; UPDATE user SET name=' 李四 ' WHERE id=1; COMMIT;(trx_id=B 的 ID)
T4 SELECT name FROM user WHERE id=1; → 张三(复用 ReadView1,B 的 ID 在 ReadView1 中属于 "未来事务",不可见)

四、MVCC 解决不可重复读的原理

不可重复读的定义:同一事务内多次读取同一行数据,结果不一致(因其他事务修改并提交)。

MVCC 解决该问题的核心逻辑:

  1. RR 隔离级:事务第一次 SELECT 生成 ReadView 后,后续所有 SELECT 复用该 ReadView;
  2. 其他事务提交的新版本,其trx_id要么大于 ReadView 的max_trx_id(未来事务),要么在m_ids中(活跃事务),根据可见性算法判定为 "不可见";
  3. 事务始终读取 ReadView 生成时已提交的版本,因此多次读取结果一致,解决不可重复读。

注意:RC 隔离级因每次 SELECT 生成新 ReadView,无法解决不可重复读。

五、MVCC + Next-key Lock 防止幻读

幻读的定义:同一事务内多次执行相同范围的 SELECT,结果集行数不一致(因其他事务插入 / 删除符合条件的行)。

1. 仅 MVCC 无法解决幻读(RR 隔离级)

即使 RR 下复用 ReadView,若其他事务插入新行并提交,新行的trx_id可能小于 ReadView 的min_trx_id(已提交),导致事务再次 SELECT 时看到新行(幻读)。

2. Next-key Lock(临键锁)的补充作用

Next-key Lock 是 InnoDB 在 RR 隔离级下默认的行锁策略,结合了Record Lock(行锁)Gap Lock(间隙锁)

  • Record Lock:锁定索引行本身;
  • Gap Lock:锁定索引行之间的间隙(防止插入新行);
  • Next-key Lock:锁定 "索引行 + 间隙",覆盖当前行和下一个间隙。

3. MVCC + Next-key Lock 解决幻读的逻辑

  • 读操作(一致性非锁定读):通过 MVCC 的 ReadView 保证多次读取的版本一致;
  • 写操作(INSERT/UPDATE/DELETE):通过 Next-key Lock 锁定范围,阻止其他事务插入 / 删除符合条件的行;
  • 两者结合:
    1. 读时:MVCC 保证只能看到事务启动时已提交的版本,看不到其他事务新增的行;
    2. 写时:Next-key Lock 阻止其他事务在锁定范围内新增 / 删除行,从根本上避免幻读。

示例(RR 隔离级 + Next-key Lock 防幻读)

时间 事务 A 事务 B
T1 BEGIN; SELECT * FROM user WHERE age > 20;(生成 ReadView1,同时加 Next-key Lock 锁定 age>20 的范围)
T2 BEGIN; INSERT INTO user (age) VALUES (25);(被 Next-key Lock 阻塞,无法插入)
T3 SELECT * FROM user WHERE age > 20; → 结果与 T1 一致(无幻读)
T4 COMMIT;(释放锁)
T5 INSERT 成功

六、总结

InnoDB 的 MVCC 实现是隐藏字段、undo-log、ReadView 三大组件的协同:

  1. 隐藏字段:标记数据版本和版本链指针;
  2. undo-log:存储历史版本,形成版本链;
  3. ReadView:根据隔离级别生成快照,通过可见性算法判断数据版本是否可见;
  4. 隔离级别差异:RC 每次 SELECT 生成 ReadView(不可重复读),RR 仅第一次生成(可重复读);
  5. 防幻读:RR 下 MVCC 保证读一致性,Next-key Lock 阻止写操作修改范围数据,两者结合彻底解决幻读。
相关推荐
小冷coding5 小时前
【MySQL】MySQL 插入一条数据的完整流程(InnoDB 引擎)
数据库·mysql
Elias不吃糖5 小时前
Java Lambda 表达式
java·开发语言·学习
情缘晓梦.6 小时前
C语言指针进阶
java·开发语言·算法
鲨莎分不晴6 小时前
Redis 基本指令与命令详解
数据库·redis·缓存
专注echarts研发20年6 小时前
工业级 Qt 业务窗体标杆实现・ResearchForm 类深度解析
数据库·qt·系统架构
南知意-7 小时前
IDEA 2025.3 版本安装指南(完整图文教程)
java·intellij-idea·开发工具·idea安装
码农水水8 小时前
蚂蚁Java面试被问:混沌工程在分布式系统中的应用
java·linux·开发语言·面试·职场和发展·php
海边的Kurisu8 小时前
苍穹外卖日记 | Day4 套餐模块
java·苍穹外卖
毕设源码-邱学长8 小时前
【开题答辩全过程】以 走失儿童寻找平台为例,包含答辩的问题和答案
java
周杰伦的稻香8 小时前
MySQL中常见的慢查询与优化
android·数据库·mysql