
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《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 ~> 隔离性底层本质总结)
-
- [3.0 隔离性底层本质](#3.0 隔离性底层本质)
- [3.1 分别阐释](#3.1 分别阐释)
-
- **读写不阻塞的本质**
- **隔离性的本质**
- [**RC 与 RR 的本质**](#RC 与 RR 的本质)
- **事务回滚的本质**
- [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:
- 对该行记录加行锁(保障写写串行化)
- 将当前行数据拷贝至 undo log,作为历史版本
- 修改最新记录的 name 为 "李四",更新
DB_TRX_ID为 10 - 将最新记录的
DB_ROLL_PTR指向 undo log 中的历史版本地址 - 事务提交,释放行锁
最终形成长度为 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:
- 对该行记录加行锁
- 将当前最新记录拷贝至 undo log 头部(头插法)
- 修改最新记录的 age 为 38,更新
DB_TRX_ID为 11 - 更新
DB_ROLL_PTR指向新的历史版本 - 事务提交,释放行锁
最终形成链表结构的版本链:
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 mode、select ... 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 |
注意
源码命名易混淆:
up_limit_id是最小值(低水位);low_limit_id是最大值边界(高水位)。
命名与直觉相反,需重点记忆。
1.5.3 版本可见性判断算法
对于版本链中的某条记录(事务 ID 为 trx_id),按以下规则判断是否对当前事务可见:
- 若
trx_id < m_up_limit_id:该事务在视图生成前已提交,可见 - 若
trx_id >= m_low_limit_id:该事务在视图生成后才启动,不可见 - 若
m_up_limit_id <= trx_id < m_low_limit_id:- 若
trx_id == m_creator_trx_id:是当前事务自身的修改,可见 - 若
trx_id不在m_ids列表中:该事务在视图生成时已提交,可见 - 若
trx_id在m_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 隔离性底层本质
- 读写不阻塞的本质:写操作修改最新版本数据,快照读读取历史版本数据,二者访问不同数据副本,因此无需加锁即可并发执行,大幅提升性能。
- 隔离性的本质:通过 MVCC 版本链 + Read View 可见性判断,让不同事务看到符合其启动时序的数据版本,实现 "先来的事务看不到后来的修改" 的隔离效果。
- RC 与 RR 的本质:仅为 Read View 的生成频率差异 ------ 一次生成全程复用 = RR,每次读取重新生成 = RC。
- 事务回滚的本质:依赖 undo log 中存储的历史版本与逆向操作,将数据恢复到修改前的状态;同时释放事务对应的 Read View 与锁资源。
3.1 分别阐释
读写不阻塞的本质
为什么两个不同的事务却可以两个现象?
读写并发好像不加锁的可以访问同一个数据,是因为有历史版本------写、当前读、比如说增删改都是当前数据,而select快照读是读的历史版本,MySQL底层MVCC机制维护多版本,所以两个不同的事务访问的压根就是不同的数据,所以就不需要加锁了------所以两个不同的事务读写就可以直接并发,这样就提高效率了。
隔离性的本质
为什么可以看到两个事务在运行的时候,尤其是手动长事务运行,最后发现一个事务更新了另一端看不到,这是怎么做到的?
同理,也是因为多版本MVCC机制的支持,因为读到的是不同的版本,老版本(历史版本)已经不变了,数据放在版本链里,读历史版本怎么能够看到最新的版本呢?在数据层面上就能够看到一种隔离性。
RC 与 RR 的本质
为什么在RR级别与RC级别的级别下会能够或者不能看到别人对应的提交?
取决于要不要重新给事务重新形成Read View。
- 不形成,就一直用老的Read View,这就叫RR,可重复读,一直读到的数据都是一样的;
- 形成,每次都重新初始化或者是重新形成Read View,这就叫RC,就有"不可重复读"的问题。
事务回滚的本质
在一个事务内部:
- 如果操作成功了,提交了;
- 如果操作失败了,回滚。
为什么能够回滚?
不就是因为有历史的相反的操作被记录下来了,版本链里面有数据,可以尽可能地进行事务的回退。
回退做两件事情:
- 事务内部对应的事务结构体和Read View对象全都释放掉;
- 将事务曾经修改过的数据全部恢复成最开始。
这个工作也是保证加锁情况下完成,这不就叫做回滚嘛!
4 ~> 扩展:写写并发与更新丢失
- 写写并发均为当前读,必须通过加锁保证串行执行,避免更新丢失。
- 两类更新丢失:
- 第一类更新丢失(回滚丢失):一个事务回滚时,覆盖了另一个事务已提交的更新;在标准隔离级别下已被杜绝。
- 第二类更新丢失(覆盖丢失):两个事务先后读取同一数据,依次修改提交,后提交的事务覆盖先提交的修改;需通过乐观锁或悲观锁避免。
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【MYSQL】MYSQL学习的一大重点:事务(中)- 隔离性与一致性
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
