【MySQL基础篇】:MySQL事务并发控制原理-MVCC机制解析

✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh--CSDN博客
✨ 文章所属专栏:MySQL篇--CSDN博客

文章目录

MVCC三个前置知识

1.表的三个隐藏字段

1.为什么要有隐藏字段?

想象一下图书管理系统:

  • 每本书不仅有书名、作者这些显式信息
  • 还需要记录谁借的、什么时候借的、在哪个书架上 这些管理信息

MySQL的表也是这样,除了我们定义的列(如id、name、balance),还有一些隐藏的管理字段来支持事务和并发控制。

2.三个隐藏字段详解

DB_TRX_ID(事务ID字段)

先补充一个知识点:事务ID

  • 每个事务都有唯一的ID:就像进程PID一样,用于系统内部管理;
  • 决定先后顺序:事务ID反映了事务的时间顺序;
  • 线性递增:越早开始的事务ID越小,全局递增;
  • 分配时机BEGIN开始事务时还没有分配;SELECT只读操作,通常也是还没有分配;只有UPDATE第一次执行写操作时,才分配事务ID;
  • 纯只读事务:通常不分配事务ID(优化性能);
  • 有写操作的事务:一定会分配事务ID;

回过来再看该字段

作用 :记录最后一次修改这行数据的事务ID

生活类比:就像商品标签上的"最后修改人"

sql 复制代码
-- 假设我们有个简单的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自动生成的唯一行标识

生活类比:就像身份证号,确保每行都有唯一标识

sql 复制代码
-- 情况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.重点理解

  1. DB_TRX_ID 告诉我们"这行数据是被哪个事务修改的";
  2. DB_ROLL_PTR 告诉我们"这行数据的历史版本在哪里" ;
  3. DB_ROW_ID 只在没有主键时才存在,作为内部标识;

这三个字段中前两个非常重要!因为它们是实现**多版本并发控制(MVCC)**的基础设施!最后一个字段我个人感觉对MVCC用处不大,了解就行。


接下来我们看看历史版本具体存储在哪里 → undo日志

2.undo日志(回滚日志)

undo日志是什么?

还记得上面提到的DB_ROLL_PTR(回滚指针)吗?它指向修改前(上一个版本)的行记录 ,这些历史版本的行记录被保存在undo日志中!

  • undo日志 = 存储容器/空间

  • 历史版本行记录 = 存储在容器中的具体内容

生活类比 :undo日志就像是照片的胶卷,记录了数据的每一个历史时刻。

undo日志的作用

作用1:支持事务回滚

当事务需要回滚时,MySQL通过undo日志恢复数据:

sql 复制代码
-- 示例操作
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执行):

sql 复制代码
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执行):

sql 复制代码
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.重点理解

  1. 每次修改都会生成一条新的undo日志记录:保存修改前的行记录
  2. 回滚指针指向保存在undo日志中的上个版本行记录:形成版本链
  3. 版本链按时间倒序排列:最新的在前,最老的在后
  4. 不同事务可以通过版本链看到不同版本的数据

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)

可见性判断过程

  1. 检查当前版本(事务300修改):

    • trx_id = 300
    • 300在m_ids[200,300]中 → 不可见
  2. 检查undo2(事务200修改):

    • trx_id = 200
    • 200在m_ids[200,300]中 → 不可见
  3. 检查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                历史版本数据
记录修改者                      
  1. 隐藏字段提供版本信息和链接关系
  2. undo日志提供历史版本数据
  3. 读视图决定应该看到哪个版本

这三者配合,就实现了多版本并发控制(MVCC)


前置知识总结

现在我们已经掌握了MVCC的三个核心前置知识:

  1. 隐藏字段:为每行数据提供版本管理信息
  2. undo日志:存储历史版本,形成版本链
  3. 读视图:决定事务能看到哪个版本的数据

这三个概念紧密配合,共同实现了MySQL的多版本并发控制机制,解决了并发事务之间的数据可见性问题!

完整示例

接下来让我们通过一个完整的示例,看看这三个概念是如何协作解决实际并发问题的:

场景:两个事务同时操作同一行数据

初始状态

复制代码
表数据:
┌────┬──────┬─────────┬─────────────┬──────────────┐
│ id │ name │ balance │ DB_TRX_ID   │ DB_ROLL_PTR  │
├────┼──────┼─────────┼─────────────┼──────────────┤
│ 1  │ 张三 │ 1000    │ 100         │ null         │
└────┴──────┴─────────┴─────────────┴──────────────┘

undo日志:(空)

时间线演示

T1: 事务200开始并读取数据

sql 复制代码
-- 事务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开始并修改数据

sql 复制代码
-- 事务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模式)

sql 复制代码
-- 事务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开始读取

sql 复制代码
-- 事务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再次读取

sql 复制代码
-- 事务300提交
COMMIT;

-- 事务400再次查询(READ COMMITTED模式会生成新读视图)
-- 但假设是REPEATABLE READ模式,仍使用T4时的读视图
SELECT balance FROM account WHERE id = 1;
-- 结果:仍然是1000(可重复读!)

关键理解

通过这个示例,我们看到:

  1. 隐藏字段的作用

    • DB_TRX_ID记录了每个版本的创建者;
    • DB_ROLL_PTR建立了版本链;
  2. undo日志的作用

    • 保存了数据的历史版本;
    • 通过版本链提供了"时光回溯"能力;
  3. 读视图的作用

    • 根据事务开始时间决定能看到哪些版本;
    • 确保了事务的隔离性;

解决并发问题的本质

复制代码
问题:两个事务同时操作同一数据,如何保证隔离性?

传统方案:加锁 → 性能差,并发度低

MVCC方案:
1. 修改时创建新版本,不删除旧版本
2. 读取时根据读视图选择合适的版本
3. 实现了"读不阻塞写,写不阻塞读"

不同隔离级别的实现差异

  • READ COMMITTED:每次读取都生成新读视图 → 能看到其他事务的最新提交;
  • REPEATABLE READ:事务内共享一个读视图 → 保证可重复读;

这就是MySQL通过三个隐藏字段 + undo日志 + 读视图实现MVCC的完整机制!

以上就是关于MySQL事务并发控制原理------MVCC机制的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!