【MySQL基础篇】:MySQL事务并发控制原理-MVCC机制解析
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:MySQL篇–CSDN博客
文章目录
- MVCC三个前置知识
- 1.表的三个隐藏字段
- DB_TRX_ID(事务ID字段)
- DB_ROLL_PTR(回滚指针字段)
- DB_ROW_ID(行ID字段)
- 2.undo日志(回滚日志)
- 作用1:支持事务回滚
- 作用2:支持MVCC(重点!)
- 3.读视图(Read View)
- 相关字段信息
- 可见性判断规则
- 生成时机(隔离级别的实现原理)
- 深入理解MVCC
- 三个概念的完整配合
- 前置知识总结
- 完整示例
MVCC三个前置知识
1.表的三个隐藏字段
1.为什么要有隐藏字段?
想象一下图书管理系统:
- 每本书不仅有书名、作者这些显式信息;
- 还需要记录谁借的、什么时候借的、在哪个书架上这些管理信息;
MySQL的表也是这样,除了我们定义的列(如id、name、balance),还有一些隐藏的管理字段来支持事务和并发控制。
2.三个隐藏字段详解
DB_TRX_ID(事务ID字段)
先补充一个知识点:事务ID
- 每个事务都有唯一的ID:就像进程PID一样,用于系统内部管理;
- 决定先后顺序:事务ID反映了事务的时间顺序;
- 线性递增:越早开始的事务ID越小,全局递增;
- 分配时机:
BEGIN
开始事务时还没有分配;SELECT
只读操作,通常也是还没有分配;只有UPDATE
第一次执行写操作时,才分配事务ID;- 纯只读事务:通常不分配事务ID(优化性能);
- 有写操作的事务:一定会分配事务ID;
回过来再看该字段
作用:记录最后一次修改这行数据的事务ID;
生活类比:就像商品标签上的"最后修改人"
-- 假设我们有个简单的account表
CREATE TABLE account(id int PRIMARY KEY,name varchar(30),balance decimal(10,2)-- 以下是MySQL自动添加的隐藏字段(我们看不见)-- DB_TRX_ID: 记录修改事务ID
);
举例说明:
实际数据行结构(包含隐藏字段):
┌────┬──────┬─────────┬─────────────┐
│ id │ name │ balance │ DB_TRX_ID │
├────┼──────┼─────────┼─────────────┤
│ 1 │ 张三 │ 100.00 │ 1001 │ ← 事务1001最后修改了这行
│ 2 │ 李四 │ 200.00 │ 1005 │ ← 事务1005最后修改了这行
└────┴──────┴─────────┴─────────────┘
DB_ROLL_PTR(回滚指针字段)
作用:指向这行数据的上一个版本(存储在undo日志中)
生活类比:就像Word文档的"版本历史"链接
形象理解:
当前版本 → DB_ROLL_PTR → 上一版本 → DB_ROLL_PTR → 更上一版本...
DB_ROW_ID(行ID字段)
作用:当表没有主键时,InnoDB自动生成的唯一行标识
生活类比:就像身份证号,确保每行都有唯一标识
-- 情况1:有主键的表(常见情况)
CREATE TABLE account(id int PRIMARY KEY, -- 有主键name varchar(30)-- MySQL不会添加DB_ROW_ID,因为已经有主键了
);-- 情况2:没有主键的表
CREATE TABLE log_table(content text,create_time datetime-- MySQL会自动添加DB_ROW_ID作为内部主键
);
3.三个字段的关系
完整的数据行结构:
┌────┬──────┬─────────┬─────────────┬──────────────┬─────────────┐
│ id │ name │ balance │ DB_TRX_ID │ DB_ROLL_PTR │ DB_ROW_ID │
├────┼──────┼─────────┼─────────────┼──────────────┼─────────────┤
│ 1 │ 张三 │ 100.00 │ 1001 │ 0x7f8b... │ (不存在) │
└────┴──────┴─────────┴─────────────┴──────────────┴─────────────┘↑ ↑ ↑我们能看到的 事务ID 指向历史版本
4.重点理解:
- DB_TRX_ID 告诉我们"这行数据是被哪个事务修改的";
- DB_ROLL_PTR 告诉我们"这行数据的历史版本在哪里" ;
- DB_ROW_ID 只在没有主键时才存在,作为内部标识;
这三个字段中前两个非常重要!因为它们是实现**多版本并发控制(MVCC)**的基础设施!最后一个字段我个人感觉对MVCC用处不大,了解就行。
接下来我们看看历史版本具体存储在哪里 → undo日志
2.undo日志(回滚日志)
undo日志是什么?
还记得上面提到的DB_ROLL_PTR
(回滚指针)吗?它指向修改前(上一个版本)的行记录,这些历史版本的行记录被保存在undo日志中!
-
undo日志 = 存储容器/空间;
-
历史版本行记录 = 存储在容器中的具体内容;
生活类比:undo日志就像是照片的胶卷,记录了数据的每一个历史时刻。
undo日志的作用
作用1:支持事务回滚
当事务需要回滚时,MySQL通过undo日志恢复数据:
-- 示例操作
BEGIN;
UPDATE account SET balance = 200 WHERE id = 1; -- 原来是100
-- 如果这时候执行 ROLLBACK,MySQL就用undo日志把balance改回100
ROLLBACK;
作用2:支持MVCC(重点!)
这是我们学习的重点:undo日志为MVCC提供历史版本数据。
1. undo日志与隐藏字段的配合
让我们通过一个具体例子来理解:
初始状态:
表中数据:
┌────┬──────┬─────────┬─────────────┬──────────────┐
│ id │ name │ balance │ DB_TRX_ID │ DB_ROLL_PTR │
├────┼──────┼─────────┼─────────────┼──────────────┤
│ 1 │ 张三 │ 100.00 │ 100 │ null │
└────┴──────┴─────────┴─────────────┴──────────────┘undo日志:(暂时为空)
第一次修改(事务200执行):
UPDATE account SET balance = 150 WHERE id = 1;
修改后的状态:
表中数据(最新版本):
┌────┬──────┬─────────┬─────────────┬──────────────┐
│ id │ name │ balance │ DB_TRX_ID │ DB_ROLL_PTR │
├────┼──────┼─────────┼─────────────┼──────────────┤
│ 1 │ 张三 │ 150.00 │ 200 │ ptr→undo1 │
└────┴──────┴─────────┴─────────────┴──────────────┘undo日志:
undo1: [事务ID:100, balance:100.00, name:张三, DB_ROLL_PTR:null]↑这里保存的是修改前的版本
第二次修改(事务300执行):
UPDATE account SET name = '张三丰' WHERE id = 1;
修改后的状态:
表中数据(最新版本):
┌────┬────────┬─────────┬─────────────┬──────────────┐
│ id │ name │ balance │ DB_TRX_ID │ DB_ROLL_PTR │
├────┼────────┼─────────┼─────────────┼──────────────┤
│ 1 │ 张三丰 │ 150.00 │ 300 │ ptr→undo2 │
└────┴────────┴─────────┴─────────────┴──────────────┘undo日志链(版本链):
undo2: [事务ID:200, balance:150.00, name:张三, DB_ROLL_PTR:ptr→undo1]↓
undo1: [事务ID:100, balance:100.00, name:张三, DB_ROLL_PTR:null]
2. 版本链的形成
undo日志中每一条历史版本行记录,通过DB_ROLL_PTR
的链接,形成了一条完整的版本链:
当前版本 → DB_ROLL_PTR → undo2 → DB_ROLL_PTR → undo1 → DB_ROLL_PTR → null↓ ↓ ↓
张三丰,150 张三,150 张三,100
(事务300) (事务200) (事务100)
3.重点理解
- 每次修改都会生成一条新的undo日志记录:保存修改前的行记录
- 回滚指针指向保存在undo日志中的上个版本行记录:形成版本链
- 版本链按时间倒序排列:最新的在前,最老的在后
- 不同事务可以通过版本链看到不同版本的数据
4. undo日志的存储特点
- 存储位置:独立的undo表空间,不在数据表中
- 内容:修改前的完整行数据+事务信息
- 生命周期:事务提交后不立即删除,供MVCC使用
- 清理时机:当没有事务需要访问这些历史版本时才清理
现在我们有了版本链,但是不同的事务应该看到哪个版本呢?这就需要 → 读视图(Read View)
3.读视图(Read View)
读视图是什么?
读视图就像是给每个事务戴上了一副"特殊眼镜",这副眼镜决定了事务能看到版本链中的哪些数据版本。
生活类比:就像电影院的3D眼镜,不同的眼镜看到不同的画面效果!
相关字段信息
读视图的核心信息
每个读视图包含4个重要信息:
1. m_ids(活跃事务列表)
记录:读视图生成时,正在执行中(未提交)的所有事务ID
作用:这些事务的修改对当前事务不可见
2. min_trx_id(最小活跃事务ID)
记录:m_ids中的最小值
作用:快速判断,小于这个ID的事务肯定已经提交了
3. max_trx_id(下一个事务ID)
记录:系统即将分配的下一个事务ID
作用:大于等于这个ID的事务肯定还没有开始
4. creator_trx_id(创建者事务ID)
记录:创建这个读视图的事务ID
作用:自己事务的修改对自己可见
可见性判断规则
当事务要读取某行数据时,会按照以下规则判断版本链中的每个版本是否可见:
对于版本链中的某个版本,其DB_TRX_ID记为trx_id:1. 如果 trx_id == creator_trx_id→ 可见(自己的修改自己能看到)2. 如果 trx_id < min_trx_id → 可见(这个事务在读视图生成前就提交了)3. 如果 trx_id >= max_trx_id→ 不可见(这个事务在读视图生成后才开始)4. 如果 min_trx_id <= trx_id < max_trx_id→ 看trx_id是否在m_ids中:- 在m_ids中 → 不可见(事务还未提交)- 不在m_ids中 → 可见(事务已经提交)
具体例子演示
让我们通过一个例子来理解读视图的工作原理:
场景设置:
- 当前系统中有事务100、200、300
- 事务100已提交,事务200、300正在执行
- 事务400刚刚开始,要读取数据
此时事务400生成的读视图:
Read View (事务400):
┌─────────────────┬─────────────────────┐
│ m_ids │ [200, 300] │ ← 正在执行的事务
│ min_trx_id │ 200 │ ← 最小活跃事务
│ max_trx_id │ 401 │ ← 下一个事务ID
│ creator_trx_id │ 400 │ ← 当前事务ID
└─────────────────┴─────────────────────┘
版本链状态:
当前版本 → undo2 → undo1↓ ↓ ↓
张三丰,150 张三,150 张三,100
(事务300) (事务200) (事务100)
可见性判断过程:
-
检查当前版本(事务300修改):
- trx_id = 300
- 300在m_ids[200,300]中 → 不可见
-
检查undo2(事务200修改):
- trx_id = 200
- 200在m_ids[200,300]中 → 不可见
-
检查undo1(事务100修改):
- trx_id = 100
- 100 < min_trx_id(200) → 可见 ✅
结果:事务400最终看到的是张三, 100
这个版本!
生成时机(隔离级别的实现原理)
读视图的生成时机这个非常重要,是隔离级别不同效果的实现机制。
时间轴模型理解
可以用一条时间轴x轴来理解读视图的工作机制:
时间轴 x轴:A点 B点 C点↓ ↓ ↓
──────┼────────────────┼────────────────┼──────→过期事务ID 活跃事务ID 未来事务ID(已提交可见) (未提交不可见) (还未开始不可见)< min_trx_id 在 m_ids 中 >= max_trx_id
READ COMMITTED(读提交):
- 每次SELECT都生成新的读视图 → 相当于每次都"重新拍照"📸📸📸;
- 原来在A-B区间的活跃事务提交后 → 移到A点左边变成"过期事务ID";
- 从"不可见"变成"可见" → 产生不可重复读现象;
- 可以看到其他事务新提交的数据;
REPEATABLE READ(可重复读):
- 事务第一次SELECT时生成读视图 → 相当于"拍一张照片用到底"📸→→→;
- 之后的SELECT都用同一个读视图 → 始终使用同一张"照片";
- 即使期间有活跃事务提交 → 读视图不变,依然看不见;
- 始终看到相同数据 → 实现可重复读现象;
本质差异:
- READ COMMITTED:每次查询都重新确定哪些事务ID属于"过期事务ID";
- REPEATABLE READ:在事务开始时就固定了"过期事务ID"的范围,不再改变;
深入理解MVCC
三个概念的完整配合
现在我们可以完整地理解这三个概念是如何配合工作的:
读视图判断规则↓
表中当前数据 → DB_ROLL_PTR → undo日志链↑ ↑
DB_TRX_ID 历史版本数据
记录修改者
- 隐藏字段提供版本信息和链接关系
- undo日志提供历史版本数据
- 读视图决定应该看到哪个版本
这三者配合,就实现了多版本并发控制(MVCC)!
前置知识总结
现在我们已经掌握了MVCC的三个核心前置知识:
- 隐藏字段:为每行数据提供版本管理信息
- undo日志:存储历史版本,形成版本链
- 读视图:决定事务能看到哪个版本的数据
这三个概念紧密配合,共同实现了MySQL的多版本并发控制机制,解决了并发事务之间的数据可见性问题!
完整示例
接下来让我们通过一个完整的示例,看看这三个概念是如何协作解决实际并发问题的:
场景:两个事务同时操作同一行数据
初始状态:
表数据:
┌────┬──────┬─────────┬─────────────┬──────────────┐
│ id │ name │ balance │ DB_TRX_ID │ DB_ROLL_PTR │
├────┼──────┼─────────┼─────────────┼──────────────┤
│ 1 │ 张三 │ 1000 │ 100 │ null │
└────┴──────┴─────────┴─────────────┴──────────────┘undo日志:(空)
时间线演示:
T1: 事务200开始并读取数据
-- 事务200开始
BEGIN; -- 事务ID = 200-- 生成读视图
Read View (事务200):
m_ids: [] (没有其他活跃事务)
min_trx_id: 201 (下一个可能的事务ID)
max_trx_id: 201
creator_trx_id: 200-- 执行查询
SELECT balance FROM account WHERE id = 1;
-- 检查当前版本:DB_TRX_ID=100 < min_trx_id=201 → 可见
-- 结果:1000
T2: 事务300开始并修改数据
-- 事务300开始
BEGIN; -- 事务ID = 300-- 修改数据
UPDATE account SET balance = 800 WHERE id = 1;
修改后的状态:
表数据(当前版本):
┌────┬──────┬─────────┬─────────────┬──────────────┐
│ id │ name │ balance │ DB_TRX_ID │ DB_ROLL_PTR │
├────┼──────┼─────────┼─────────────┼──────────────┤
│ 1 │ 张三 │ 800 │ 300 │ ptr→undo1 │ ← 新版本
└────┴──────┴─────────┴─────────────┴──────────────┘undo日志:
undo1: [DB_TRX_ID:100, name:张三, balance:1000, DB_ROLL_PTR:null] ← 历史版本
T3: 事务200再次读取(REPEATABLE READ模式)
-- 事务200再次查询(仍使用T1时的读视图)
SELECT balance FROM account WHERE id = 1;-- 使用T1时的读视图判断:
-- 1. 检查当前版本:DB_TRX_ID=300 >= max_trx_id=201 → 不可见
-- 2. 沿着DB_ROLL_PTR找到undo1:DB_TRX_ID=100 < min_trx_id=201 → 可见
-- 结果:1000 (和第一次查询结果一致!)
T4: 新事务400开始读取
-- 事务400开始
BEGIN; -- 事务ID = 400-- 生成新的读视图
Read View (事务400):
m_ids: [200, 300] (当前活跃的事务)
min_trx_id: 200
max_trx_id: 401
creator_trx_id: 400-- 执行查询
SELECT balance FROM account WHERE id = 1;
-- 1. 检查当前版本:DB_TRX_ID=300在m_ids中 → 不可见
-- 2. 检查undo1:DB_TRX_ID=100 < min_trx_id=200 → 可见
-- 结果:1000
T5: 事务300提交后,事务400再次读取
-- 事务300提交
COMMIT;-- 事务400再次查询(READ COMMITTED模式会生成新读视图)
-- 但假设是REPEATABLE READ模式,仍使用T4时的读视图
SELECT balance FROM account WHERE id = 1;
-- 结果:仍然是1000(可重复读!)
关键理解
通过这个示例,我们看到:
-
隐藏字段的作用:
DB_TRX_ID
记录了每个版本的创建者;DB_ROLL_PTR
建立了版本链;
-
undo日志的作用:
- 保存了数据的历史版本;
- 通过版本链提供了"时光回溯"能力;
-
读视图的作用:
- 根据事务开始时间决定能看到哪些版本;
- 确保了事务的隔离性;
解决并发问题的本质
问题:两个事务同时操作同一数据,如何保证隔离性?传统方案:加锁 → 性能差,并发度低MVCC方案:
1. 修改时创建新版本,不删除旧版本
2. 读取时根据读视图选择合适的版本
3. 实现了"读不阻塞写,写不阻塞读"
不同隔离级别的实现差异
- READ COMMITTED:每次读取都生成新读视图 → 能看到其他事务的最新提交;
- REPEATABLE READ:事务内共享一个读视图 → 保证可重复读;
这就是MySQL通过三个隐藏字段 + undo日志 + 读视图实现MVCC的完整机制!
以上就是关于MySQL事务并发控制原理——MVCC机制的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!