当前位置: 首页 > news >正文

【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.重点理解

  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日志恢复数据:

-- 示例操作
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.重点理解

  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开始并读取数据

-- 事务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(可重复读!)

关键理解

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

  1. 隐藏字段的作用

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

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

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

解决并发问题的本质

问题:两个事务同时操作同一数据,如何保证隔离性?传统方案:加锁 → 性能差,并发度低MVCC方案:
1. 修改时创建新版本,不删除旧版本
2. 读取时根据读视图选择合适的版本
3. 实现了"读不阻塞写,写不阻塞读"

不同隔离级别的实现差异

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

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

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

http://www.lryc.cn/news/613337.html

相关文章:

  • 安卓开发:网络状态监听封装的奥秘
  • 力扣 hot100 Day68
  • 关于vue2中对接海康摄像头以及直播流rtsp或rtmp,后台ffmpeg转码后通过ws实现
  • ADC、Flash、SPI、watchdog
  • Linux 磁盘中的文件
  • 多线程问题,子线程同时操作全局变量,使用后需要清空吗 ?
  • 容器之王--部署Docker私有仓库harbor母盘步骤演练
  • 小米前端笔试和面试
  • AI日报0807 | GPT-5或今晚1点来袭:四大版本全曝光
  • 使用Ollama本地部署DeepSeek、GPT等大模型
  • 13-netty基础-手写rpc-消费方生成代理-05
  • 车辆特征与车牌识别准确率↑29%:陌讯多模态融合算法实战解析
  • [spring-cloud: 动态刷新]-源码分析
  • 基于MATLAB实现支持向量机(SVM)分类
  • android 之 Kotlin中Handler的使用
  • 栅栏密码的加密解密原理
  • zookeeper因jute.maxbuffer启动异常问题排查处理
  • 使用 decimal 包解决 go float 浮点数运算失真
  • 可执行文件的生成与加载执行
  • Linux的进程间通信
  • 嵌入式学习硬件(一)ARM体系架构
  • 简单手写Transformer:原理与代码详解
  • Java中的反射机制
  • 土壤盐分传感器与土壤电导率传感器直接的关系
  • 深入理解String类:揭秘Java字符串常量池的优化机制
  • 【2025最新版】火狐浏览器(官方版)安装-附教程
  • 飞算JavaAI深度解析:Java开发者的智能革命
  • AUTOSAR进阶图解==>AUTOSAR_EXP_BSWDistributionGuide
  • 损耗对信号质量的影响
  • Java 八大经典排序算法全解析