【MYSQL】MYSQL学习的一大重点:事务(下)- InnoDB 事务隔离性原理(MVCC 视角)

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • [0 ~> 前置:数据库并发场景分类](#0 ~> 前置:数据库并发场景分类)
    • [0.1 三类并发场景](#0.1 三类并发场景)
    • [0.2 核心前提](#0.2 核心前提)
  • [1 ~> 核心机制:MVCC 多版本并发控制](#1 ~> 核心机制:MVCC 多版本并发控制)
    • [1.1 MVCC 基础定义](#1.1 MVCC 基础定义)
    • [1.2 前置组件 1:行记录隐藏字段](#1.2 前置组件 1:行记录隐藏字段)
    • [1.3 前置组件 2:undo 回滚日志](#1.3 前置组件 2:undo 回滚日志)
      • [1.3.1 基础定义](#1.3.1 基础定义)
      • [1.3.2 存储特性](#1.3.2 存储特性)
    • [1.4 行数据版本链的构建流程](#1.4 行数据版本链的构建流程)
      • [1.4.1 初始状态](#1.4.1 初始状态)
      • [1.4.2 第一次修改(事务 10)](#1.4.2 第一次修改(事务 10))
      • [1.4.3 第二次修改(事务 11)](#1.4.3 第二次修改(事务 11))
      • [1.4.4 delete 与 insert 的版本特性](#1.4.4 delete 与 insert 的版本特性)
    • [1.5 前置组件 3:Read View 读视图](#1.5 前置组件 3:Read View 读视图)
      • [1.5.1 核心定义](#1.5.1 核心定义)
      • [1.5.2 Read View 核心字段](#1.5.2 Read View 核心字段)
      • [1.5.3 版本可见性判断算法](#1.5.3 版本可见性判断算法)
  • [2 ~> RC 与 RR 隔离级别的本质差异](#2 ~> RC 与 RR 隔离级别的本质差异)
    • [2.1 现象对比](#2.1 现象对比)
    • [2.2 底层根因:Read View 生成策略](#2.2 底层根因:Read View 生成策略)
      • [2.2.1 RR(可重复读)级别](#2.2.1 RR(可重复读)级别)
      • [2.2.2 RC(读提交)级别](#2.2.2 RC(读提交)级别)
    • [2.3 关键实验验证](#2.3 关键实验验证)
      • [实验 1:RR 级别下提前触发快照读](#实验 1:RR 级别下提前触发快照读)
      • [实验 2:RR 级别下延迟触发快照读](#实验 2:RR 级别下延迟触发快照读)
  • [3 ~> 隔离性底层本质总结](#3 ~> 隔离性底层本质总结)
  • [4 ~> 扩展:写写并发与更新丢失](#4 ~> 扩展:写写并发与更新丢失)
  • 结尾


0 ~> 前置:数据库并发场景分类

0.1 三类并发场景

  • 读 - 读并发:不存在线程安全问题,无需并发控制
  • 读 - 写并发:存在隔离性问题,可能出现脏读、不可重复读、幻读;是隔离级别研究的核心场景,由 MVCC 机制解决
  • 写 - 写并发:存在线程安全问题,可能出现第一类 / 第二类更新丢失;通过加锁机制保证数据安全,非隔离级别研究重点

0.2 核心前提

事务具备严格的先后顺序,通过单向增长的事务 ID区分:事务 ID 数值越小,代表事务启动越早;数值越大,代表启动越晚。


1 ~> 核心机制:MVCC 多版本并发控制

1.1 MVCC 基础定义

MVCC(Multi-Version Concurrency Control,多版本并发控制)是一种解决读 - 写冲突的无锁并发控制方案。

  • 核心思想:为每个修改保存一个关联事务 ID 的版本,读操作读取事务启动前的数据库快照,读写操作访问不同数据版本,互不阻塞
  • 核心价值:
    • 读操作不阻塞写操作,写操作不阻塞读操作,大幅提升数据库并发读写性能
    • 支撑事务隔离性,解决脏读、不可重复读,在特定隔离级别下可避免幻读
    • 支撑事务回滚能力

注意:

  • 纯快照读场景下,InnoDB 的 RR 级别通过 MVCC 可以避免幻读;
  • 当前读场景下,需要配合 Next-Key Lock(间隙锁 + 行锁)才能解决幻读;
  • RC 级别下,即使是快照读也无法避免幻读。 MVCC 本身不直接解决 "更新丢失" 问题,更新丢失属于写写并发范畴,由锁机制处理。

1.2 前置组件 1:行记录隐藏字段

InnoDB 聚簇索引的每一行记录,除了用户定义的字段外,会自动添加 3 个核心隐藏字段 + 行头删除标记:

隐藏字段 字节数 作用
DB_TRX_ID 6 byte 最近一次创建 / 修改该行记录的事务 ID
DB_ROLL_PTR 7 byte 回滚指针,指向该行上一个历史版本(存储于 undo log 中)
DB_ROW_ID 6 byte 隐藏自增主键;若表未定义主键,InnoDB 会自动以此字段生成聚簇索引;若表有显式主键,该字段不生效
删除标志位 行头内标记 记录删除标记,删除操作仅修改该标志位,而非物理删除数据

有的说法是"有 4 个隐藏列字段",标准表述为 3 个独立隐藏列(DB_TRX_ID、DB_ROLL_PTR、DB_ROW_ID),删除标志位属于行记录头信息中的标记位,不属于独立列字段------为了简化理解,可以理解成"有 4 个隐藏列字段",不过不影响核心逻辑。

1.3 前置组件 2:undo 回滚日志

1.3.1 基础定义

undo log 是 InnoDB 用于实现事务回滚、MVCC 版本链的日志模块。

  • 核心作用:1. 支撑事务回滚(保障原子性);2. 存储行数据的历史版本,支撑 MVCC 快照读
  • 存储内容:行数据修改前的历史版本,以及对应逆向操作(如 insert 对应 delete、update 对应反向 update)

1.3.2 存储特性

原始笔记简化表述为 "undo log 是 MySQL 中的一段内存缓冲区",【审计修正】如下:

  • undo log 同时存在内存缓冲区(undo log buffer)与磁盘持久化文件,并非纯内存结构;
  • 事务提交后,undo log 不会立即被释放,由后台 purge 线程负责清理,需确保没有任何事务再引用该历史版本时才会被回收;
  • undo log 分为 insert undo(仅用于回滚,提交后可直接删除)和 update undo(用于回滚和 MVCC,提交后不能立即删除)。

1.4 行数据版本链的构建流程

student 表的一条记录为例,版本链构建遵循写时拷贝 + 头插法原则:

1.4.1 初始状态

9 号事务插入初始记录,版本链只有一条数据:

Plain 复制代码
最新记录:(name=张三, age=28, DB_TRX_ID=9, DB_ROLL_PTR=null)
undo log:无历史版本

1.4.2 第一次修改(事务 10)

事务 10 执行 update student set name='李四' where id=1

  1. 对该行记录加行锁(保障写写串行化)
  2. 将当前行数据拷贝至 undo log,作为历史版本
  3. 修改最新记录的 name 为 "李四",更新 DB_TRX_ID 为 10
  4. 将最新记录的 DB_ROLL_PTR 指向 undo log 中的历史版本地址
  5. 事务提交,释放行锁

最终形成长度为 2 的版本链:

Plain 复制代码
最新记录:(name=李四, age=28, DB_TRX_ID=10, DB_ROLL_PTR=0xaa)
  → undo log 历史版本1:(name=张三, age=28, DB_TRX_ID=9, DB_ROLL_PTR=null)

1.4.3 第二次修改(事务 11)

事务 11 执行 update student set age=38 where id=1

  1. 对该行记录加行锁
  2. 将当前最新记录拷贝至 undo log 头部(头插法)
  3. 修改最新记录的 age 为 38,更新 DB_TRX_ID 为 11
  4. 更新 DB_ROLL_PTR 指向新的历史版本
  5. 事务提交,释放行锁

最终形成链表结构的版本链:

Plain 复制代码
最新记录:(name=李四, age=38, DB_TRX_ID=11, DB_ROLL_PTR=0xbb)
  → 历史版本1:(name=李四, age=28, DB_TRX_ID=10, DB_ROLL_PTR=0xaa)
    → 历史版本2:(name=张三, age=28, DB_TRX_ID=9, DB_ROLL_PTR=null)

1.4.4 delete 与 insert 的版本特性

  • delete 操作:不物理删除数据,仅修改删除标志位,同样会形成版本链
  • insert 操作:无前驱历史版本,但会在 undo log 记录对应 delete 逆向操作,用于事务回滚;事务提交后,insert undo 可被直接清理

1.5 前置组件 3:Read View 读视图

1.5.1 核心定义

Read View 是事务执行快照读时生成的读视图,记录生成时刻系统中活跃的事务状态,用于判断行数据版本对当前事务的可见性。

  • 本质:一个用于可见性判断的数据结构,事务的快照读结果完全由 Read View + 版本链共同决定
  • 读取模式分类:
    • 快照读:普通 select 语句,读取历史版本,不加锁,依赖 MVCC
    • 当前读 :增删改、select ... lock in share modeselect ... for update,读取最新版本,需要加锁

1.5.2 Read View 核心字段

字段 含义
m_ids 生成 Read View 时刻,系统中所有活跃(未提交)事务的 ID 列表
m_up_limit_id 低水位:m_ids 中最小的事务 ID;小于该值的事务均已提交,全部可见
m_low_limit_id 高水位:生成 Read View 时刻,系统下一个待分配的事务 ID(即已出现的最大事务 ID + 1);大于等于该值的事务均在视图生成后启动,全部不可见
m_creator_trx_id 创建当前 Read View 的事务自身的 ID
注意

源码命名易混淆:

  1. up_limit_id 是最小值(低水位);
  2. low_limit_id 是最大值边界(高水位)。

命名与直觉相反,需重点记忆。

1.5.3 版本可见性判断算法

对于版本链中的某条记录(事务 ID 为 trx_id),按以下规则判断是否对当前事务可见:

  1. trx_id < m_up_limit_id:该事务在视图生成前已提交,可见
  2. trx_id >= m_low_limit_id:该事务在视图生成后才启动,不可见
  3. m_up_limit_id <= trx_id < m_low_limit_id
    1. trx_id == m_creator_trx_id:是当前事务自身的修改,可见
    2. trx_id 不在 m_ids 列表中:该事务在视图生成时已提交,可见
    3. trx_idm_ids 列表中:该事务在视图生成时仍活跃(未提交),不可见

若当前版本不可见,则通过 DB_ROLL_PTR 遍历上一个历史版本,重复上述判断,直到找到可见版本;若遍历完所有版本均不可见,则返回空。

对应源码核心逻辑(C++ 简化版):

cpp 复制代码
/**
 * @brief 判断事务ID对应的数据版本是否对当前视图可见
 * @param id 待判断的行记录事务ID
 * @return true 可见,false 不可见
 */
bool changes_visible(trx_id_t id) const {
    // 1. 小于低水位:历史已提交事务,必然可见;自身事务修改也可见
    if (id < m_up_limit_id || id == m_creator_trx_id) {
        return true;
    }

    // 2. 大于等于高水位:视图生成后才出现的事务,必然不可见
    if (id >= m_low_limit_id) {
        return false;
    } 
    // 3. 无活跃事务:所有事务均已提交,全部可见
    else if (m_ids.empty()) {
        return true;
    }

    // 4. 中间区间:二分查找判断是否在活跃事务列表中
    // 在列表中 = 未提交 = 不可见;不在列表中 = 已提交 = 可见
    return !std::binary_search(m_ids.data(), m_ids.data() + m_ids.size(), id);
}

2 ~> RC 与 RR 隔离级别的本质差异

2.1 现象对比

以两个并发事务 A、B 为例:

  • 事务 A:修改数据并提交
  • 事务 B:在事务 A 提交前后各执行一次快照读
隔离级别 事务 B 第一次快照读 事务 A 提交后,事务 B 第二次快照读 现象
RC(读提交) 读到旧版本 读到事务 A 提交的新版本 不可重复读
RR(可重复读) 读到旧版本 仍读到旧版本 可重复读

2.2 底层根因:Read View 生成策略

RC 与 RR 的唯一核心区别,就是 Read View 的生成时机不同。

2.2.1 RR(可重复读)级别

  • 策略:事务内仅在第一次快照读时生成一个 Read View,后续所有快照读都复用同一个 Read View
  • 效果:事务生命周期内,可见性基准始终不变;只要第一次快照读时其他事务还未提交,后续无论其他事务是否提交,当前事务都看不到其修改,从而实现可重复读。

2.2.2 RC(读提交)级别

  • 策略:事务内每次执行快照读,都会重新生成一个全新的 Read View
  • 效果:每次快照读都以当前最新的系统事务状态为基准;其他事务一旦提交,下一次快照读就能看到其修改,因此存在不可重复读问题。

2.3 关键实验验证

实验 1:RR 级别下提前触发快照读

事务 A 事务 B(RR 级别)
begin begin
select * from user; // 第一次快照读,生成 Read View,此时 A 活跃
update user set age=18 where id=1;
commit;
select * from user; // 复用旧 Read View,看不到 A 的修改,仍为旧值
select * from user lock in share mode; // 当前读,读到最新值 18

实验 2:RR 级别下延迟触发快照读

事务 A 事务 B(RR 级别)
begin begin
update user set age=28 where id=1;
commit;
select * from user; // 第一次快照读,生成 Read View,此时 A 已提交
读到 age=28,即事务 A 的修改

结论:RR 级别下,事务的可见性基准由第一次快照读的时机决定,而非事务启动时机。


3 ~> 隔离性底层本质总结

3.0 隔离性底层本质

  1. 读写不阻塞的本质:写操作修改最新版本数据,快照读读取历史版本数据,二者访问不同数据副本,因此无需加锁即可并发执行,大幅提升性能。
  2. 隔离性的本质:通过 MVCC 版本链 + Read View 可见性判断,让不同事务看到符合其启动时序的数据版本,实现 "先来的事务看不到后来的修改" 的隔离效果。
  3. RC 与 RR 的本质:仅为 Read View 的生成频率差异 ------ 一次生成全程复用 = RR,每次读取重新生成 = RC。
  4. 事务回滚的本质:依赖 undo log 中存储的历史版本与逆向操作,将数据恢复到修改前的状态;同时释放事务对应的 Read View 与锁资源。

3.1 分别阐释

读写不阻塞的本质

为什么两个不同的事务却可以两个现象?

读写并发好像不加锁的可以访问同一个数据,是因为有历史版本------写、当前读、比如说增删改都是当前数据,而select快照读是读的历史版本,MySQL底层MVCC机制维护多版本,所以两个不同的事务访问的压根就是不同的数据,所以就不需要加锁了------所以两个不同的事务读写就可以直接并发,这样就提高效率了。

隔离性的本质

为什么可以看到两个事务在运行的时候,尤其是手动长事务运行,最后发现一个事务更新了另一端看不到,这是怎么做到的?

同理,也是因为多版本MVCC机制的支持,因为读到的是不同的版本,老版本(历史版本)已经不变了,数据放在版本链里,读历史版本怎么能够看到最新的版本呢?在数据层面上就能够看到一种隔离性。

RC 与 RR 的本质

为什么在RR级别与RC级别的级别下会能够或者不能看到别人对应的提交?

取决于要不要重新给事务重新形成Read View。

  1. 不形成,就一直用老的Read View,这就叫RR,可重复读,一直读到的数据都是一样的;
  2. 形成,每次都重新初始化或者是重新形成Read View,这就叫RC,就有"不可重复读"的问题。

事务回滚的本质

在一个事务内部:

  1. 如果操作成功了,提交了;
  2. 如果操作失败了,回滚。

为什么能够回滚?

不就是因为有历史的相反的操作被记录下来了,版本链里面有数据,可以尽可能地进行事务的回退。

回退做两件事情:

  1. 事务内部对应的事务结构体和Read View对象全都释放掉;
  2. 将事务曾经修改过的数据全部恢复成最开始。

这个工作也是保证加锁情况下完成,这不就叫做回滚嘛!


4 ~> 扩展:写写并发与更新丢失

  • 写写并发均为当前读,必须通过加锁保证串行执行,避免更新丢失。
  • 两类更新丢失:
    • 第一类更新丢失(回滚丢失):一个事务回滚时,覆盖了另一个事务已提交的更新;在标准隔离级别下已被杜绝。
    • 第二类更新丢失(覆盖丢失):两个事务先后读取同一数据,依次修改提交,后提交的事务覆盖先提交的修改;需通过乐观锁或悲观锁避免。

结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

【MYSQL】MYSQL学习的一大重点:事务(中)- 隔离性与一致性

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
三少爷的鞋1 小时前
launch 不是用来切线程的:为什么我越来越少在它后面写 Dispatchers.IO
android
爱和冰阔落1 小时前
【Linux】从匿名管道到进程池:任务派发、fd 继承 Bug 与完整实现
android·linux·运维·c++
懿路向前1 小时前
【HarmonyOS学习笔记】2026-08-05 | 端插件卡片绑定与跨上下文判断
笔记·学习·ai编程·harmonyos
小园子的小菜2 小时前
Redis 核心原理深度解析:从数据结构、IO 模型到持久化机制
数据库·redis·缓存
YUS云生2 小时前
大模型学习·第44天:Chroma向量数据库与RAG链式组装
数据库·学习
weixin_440784119 小时前
【HandlerThread实现原理】
android·java·开发语言
毛驴赶鹿9 小时前
【金仓数据库征文】KES V9性能调优实战记录:从慢SQL排查到全链路优化
数据库·sql
阿坤带你走近大数据9 小时前
基金面试准备1
面试·职场和发展
码农阿豪9 小时前
【金仓数据库征文】Mac开发环境|SpringBoot3 + MyBatisPlus对接KES V9 Docker实战全指南
数据库·macos·docker