尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

【MySQL】一文读懂 MySQL 事务控制与 MVCC 多版本并发控制底层原理

【MySQL】一文读懂 MySQL 事务控制与 MVCC 多版本并发控制底层原理
📅 发布时间:2026/7/4 20:03:01

🔥个人主页:Cx330🌸

❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》

《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔

《Git深度解析》:版本管理实战全解 《Qt 极境架构》MySQL 核心技术与实战

🌟心向往之行必能


🎥Cx330🌸的简介:


目录

前言

一、 从售票系统说起:CURD 不加控制会有什么问题?

并发冲突场景:

如何解决上述问题?

二、 什么是事务(Transaction)?

1. 事务的定义

2. 事务的核心属性:ACID

3. 为什么会出现事务?

4. 事务的版本支持

三、 事务提交方式与基本操作

1. 事务的提交方式

2. 事务常用操作实战

实验一:正常演示(开始、保存点与回滚)

实验二:未 Commit 客户端崩溃(异常自动回滚)

实验三:Commit 之后客户端崩溃(持久化成功)

实验四:begin 会自动忽略 autocommit 设置

实验五:单条 SQL 与事务的关系

四、 深入探究:事务隔离级别与并发问题

1. 为什么要有隔离性?

2. 四大隔离级别与 anomalies(并发缺陷)

3. 查看与设置隔离级别

4. 并发事务带来的 3 个核心问题

五、四 种隔离级别实操演示

5.1 读未提交(read uncommitted)

5.2 读已提交(read committed)

5.3 可重复读(repeatable read)

5.4 串行化(serializable)

六. 一致性:事务的最终归宿

七、 硬核底层:MVCC(多版本并发控制)原理

1. 理解 MVCC 的三个核心基石

核心基石一:3 个记录隐藏字段

核心基石二:Undo Log 与历史版本链

【模拟演练】版本链的形成过程:

核心基石三:Read View 的可见性算法

可见性判断算法图解:

2. 探究:RC 与 RR 的本质区别是什么?

实例对比(测试用例):

测试用例 1 (在 RR 隔离级别下):

测试用例 2 (在 RR 隔离级别下):

结语


前言

在多线程高并发的后端开发中,数据安全与并发控制永远是重中之重。无论是面试中必问的“高频考点”,还是实际业务(如电商秒杀、票务系统)中的核心逻辑,MySQL 事务(Transaction)及其隔离机制都是每一位优秀程序员必须掌握的看家本领。

今天,博主将带大家完全拆解 MySQL 事务的底层世界,从最直观的售票系统并发缺陷切入,横跨 ACID 属性、事务实战操作、四大隔离级别,最终直击最硬核的MVCC(多版本并发控制)与 Read View 机制。文章干货极多,建议收藏后反复研读!


一、 从售票系统说起:CURD 不加控制会有什么问题?

想象一个经典的火车票售票系统,数据库中有一张tickets表:

id

name

nums

10

西安 <-> 兰州

1

此时有客户端 A 和客户端 B 同时尝试买票,两者的执行逻辑如下:

if (nums > 0) { // 卖票逻辑... update tickets set nums = nums - 1 where id = 10; }

并发冲突场景:

  1. 客户端 A检查数据库,发现nums = 1 > 0,决定卖票(步骤 1)。

  2. 在 A 还没有来得及更新数据库时,客户端 B也进来检查,发现nums依然是 1,也决定卖票(步骤 2、3)。

  3. A 执行更新,将nums改为 0(步骤 4)。

  4. B 也执行更新,将nums改为 0(步骤 5)。

结果:同一张票被卖了两次(超卖问题)!

如何解决上述问题?

要彻底解决这种并发混乱,购票操作必须满足以下属性:

  1. 原子性:买票的过程必须是一个整体,不能停在中间。

  2. 隔离性:客户端之间的买票操作互相不应该有干扰。

  3. 持久性:买票成功后,数据的修改必须是永久有效的。

  4. 一致性:买票前和买票后,数据库的状态都必须是确定的、符合业务规则的。

这,正是事务(Transaction)诞生的根本原因。

二、 什么是事务(Transaction)?

1. 事务的定义

事务是指由一组 DML(数据操作语言)语句组成的逻辑处理单元。这些语句在逻辑上存在强相关性,要么全部成功,要么全部失败,是一个不可分割的整体。

生活场景:比如你毕业了,学校的教务系统要删除你在学校的所有信息(基本信息、成绩、论坛文章等)。这就需要多条DELETE语句,这些语句必须构成一个事务。如果基本信息删了,成绩却因为网络断开没删掉,就会产生脏数据。

2. 事务的核心属性:ACID

  • 原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。事务在执行过程中出错,会被回滚(Rollback)到事务开始前的状态,如同从未执行过一样。

  • 一致性(Consistency):在事务开始之前和结束以后,数据库的完整性没有被破坏。这属于业务层的最终目的,通过 AID 三个技术特性共同保证。

  • 隔离性(Isolation):数据库允许多个并发事务同时对数据进行读写。隔离性可以防止并发交叉执行时导致的数据不一致。隔离性分为不同级别(RU、RC、RR、Serializable)。

  • 持久性(Durability):事务一旦提交(Commit),其对数据的修改就是永久性的,即使发生系统崩溃、断电,数据也不会丢失。

3. 为什么会出现事务?

事务本质上是 MySQL 开发者为了简化应用层编程模型而设计的。它让业务层不需要考虑复杂的网络异常、服务器宕机、并发冲突等底层细节。在写业务代码时,我们只需要简单地决定是“提交(Commit)”还是“回滚(Rollback)”即可。

4. 事务的版本支持

在 MySQL 中,只有使用了 InnoDB 存储引擎的表才支持事务,传统的 MyISAM 引擎是不支持事务的。我们可以通过以下命令查看:

show engines;

输出中可以看到:

  • InnoDB->Transactions: YES(支持事务、行级锁、外键)

  • MyISAM->Transactions: NO(不支持事务)

三、 事务提交方式与基本操作

1. 事务的提交方式

MySQL 事务提交方式分为两种:

  • 自动提交(Autocommit)

  • 手动提交

查看自动提交配置:

show variables like 'autocommit';
  • Value = ON:表示默认自动提交,即执行单条 DML 语句后,MySQL 会自动将其提交。

  • 修改方式:

    SET AUTOCOMMIT=0; -- 禁止自动提交 SET AUTOCOMMIT=1; -- 开启自动提交

2. 事务常用操作实战

为了便于直观观察,我们先在会话中把隔离级别设置为读未提交:

set global transaction isolation level READ UNCOMMITTED; -- 注意:需要重启客户端生效

创建测试表:

create table if not exists account( id int primary key, name varchar(50) not null default '', blance decimal (10,2) not null default 0.0 )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
实验一:正常演示(开始、保存点与回滚)
mysql> start transaction; -- 开启事务(或使用 begin) Query OK, 0 rows affected (0.00 sec) mysql> savepoint savel; -- 创建保存点 savel Query OK, 0 rows affected (0.00 sec) mysql> insert into account values (1, '张三', 100); Query OK, 1 row affected (0.05 sec) mysql> savepoint save2; -- 创建保存点 save2 Query OK, 0 rows affected (0.01 sec) mysql> insert into account values (2, '李四', 10000); Query OK, 1 row affected (0.00 sec) mysql> select * from account; +----+--------+----------+ | id | name | blance | +----+--------+----------+ | 1 | 张三 | 100.00 | | 2 | 李四 | 10000.00 | +----+--------+----------+ mysql> rollback to save2; -- 回滚到保存点 save2 Query OK, 0 rows affected (0.03 sec) mysql> select * from account; -- 李四这条记录回滚消失了 +----+--------+----------+ | id | name | blance | +----+--------+----------+ | 1 | 张三 | 100.00 | +----+--------+----------+ mysql> rollback; -- 直接 rollback 回滚到最开始位置 Query OK, 0 rows affected (0.00 sec) mysql> select * from account; Empty set (0.00 sec) -- 数据全部清空,回滚成功!

实验二:未 Commit 客户端崩溃(异常自动回滚)
  • 终端 A:开启事务begin,插入“张三”,但不提交(未执行commit)。

  • 终端 B:因为隔离级别是 RU,此时能查到“张三”这条记录。

  • 终端 A:突然遭遇异常(比如使用ctrl + \或kill强行终止客户端)。

  • 终端 B:再次查询,发现“张三”已经不见了。

  • 结论:如果事务未 Commit,一旦客户端崩溃断开,MySQL 会自动对该事务执行回滚操作,保证原子性。

实验三:Commit 之后客户端崩溃(持久化成功)
  • 终端 A:开启事务begin,插入“张三”,并执行commit。

  • 终端 A:异常强杀客户端。

  • 终端 B:再次查询,“张三”数据依然存在。

  • 结论:一旦事务被commit提交,数据就会被持久化到磁盘上,无法再通过rollback撤销。

实验四:begin 会自动忽略 autocommit 设置

实验五:单条 SQL 与事务的关系

很多人误以为不显式写begin就没有事务。其实:

  • 如果设置了autocommit = 1(自动提交模式开启),执行单条 SQL 时,InnoDB 会自动将这条 SQL 包装成一个事务并立即提交。如果客户端在执行过程中崩溃,只要执行成功,数据就会持久化。

  • 如果设置了autocommit = 0(关闭自动提交),即便是一条简单的INSERT,在终端异常挂掉后,由于没有执行commit,MySQL 也会直接在后台将其回滚。

终极结论:

  1. 只要输入了begin或start transaction,事务就必须通过commit提交才会持久化,不受全局autocommit变量的影响。

  2. MySQL 的单条 DML 语句在 InnoDB 下默认都是当成独立事务自动提交的。


四、 深入探究:事务隔离级别与并发问题

1. 为什么要有隔离性?

MySQL 作为一个高性能的网络服务,随时可能有成百上千个客户端发起并发事务。如果大家同时修改同一张表、同一行数据,在不加保护的前提下必然会乱套。

为了在高并发性能与数据安全性之间取得平衡,MySQL 引入了隔离性,并提供了四种不同的隔离级别。

2. 四大隔离级别与 anomalies(并发缺陷)

  1. 读未提交(Read Uncommitted, RU):

    • 特点:没有任何真正的隔离保护,效率最高,但基本不加锁。

    • 致命缺陷:存在脏读(Dirty Read)。即一个事务在运行中,能读取到另一个事务更新了、但尚未提交(Uncommitted)的数据。如果另一个事务最后回滚了,那读取到的就是虚假数据。

  2. 读提交(Read Committed, RC):

    • 特点:一个事务只能看到其他事务已经提交(Committed)的修改。这也是绝大多数数据库(如 Oracle)默认的隔离级别。

    • 缺陷:存在不可重复读(Non-repeatable Read)。在同一个事务内,多次执行相同的SELECT查询,可能会因为其他事务在此期间提交了修改,导致前后读取到的数据内容不一致。

  3. 可重复读(Repeatable Read, RR):

    • 特点:MySQL 的默认隔离级别。它确保在同一个事务内,多次读取同一行数据,看到的结果始终是一致的。

    • 理论缺陷:存在幻读(Phantom Read)问题。即当事务 A 进行范围查询时,事务 B 在该范围内插入(Insert)了新记录并提交,事务 A 再次读取该范围时,会莫名其妙多出一些记录,就像产生了幻觉。

    • 注意:MySQL 的 InnoDB 引擎在 RR 级别下,通过Next-Key Locks(GAP 间隙锁 + 行锁)已经很大程度上解决了幻读问题!

  4. 串行化(Serializable):

    • 特点:最高隔离级别。所有并发事务会被强制排队串行化执行。

    • 机制:在读的数据行上全部加共享锁,读取也会被阻塞。

    • 代价:并发性能极低,实际生产中几乎从不使用。

3. 查看与设置隔离级别

  • 查看当前隔离级别:

    SELECT @@global.tx_isolation; -- 查看全局级别 SELECT @@session.tx_isolation; -- 查看当前会话级别 SELECT @@tx_isolation; -- 默认同上
  • 设置隔离级别语法:

    SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | S

4. 并发事务带来的 3 个核心问题

在讲解隔离级别之前,我们先搞懂并发事务会带来的 3 个经典问题,这也是面试必考点:

问题名称定义说明
脏读一个事务读到了另一个事务未提交的修改数据。如果另一个事务回滚,读到的数据就是无效的脏数据。
不可重复读同一个事务内,多次执行相同的 select 语句,读到的结果不一样。核心原因是其他事务对数据做了修改 / 删除并提交。
幻读同一个事务内,多次执行相同的 select 语句,第二次读到了第一次没有的新增记录,就像出现了幻觉。核心原因是其他事务做了新增并提交。

五、四 种隔离级别实操演示

所有演示均基于account表,初始数据:

truncate table account; insert into account values (1, '张三', 100.00), (2, '李四', 10000.00);

5.1 读未提交(read uncommitted)

这是最低的隔离级别,几乎没有隔离性,会出现脏读,生产环境严禁使用

核心问题:脏读,读到了其他事务未提交的数据,一旦其他事务回滚,数据就失效了。

5.2 读已提交(read committed)

这是大多数数据库(Oracle 、SQL Server)的默认隔离级别,解决了脏读,但会出现不可重复读。

核心问题:不可重复读,同一个事务内,相同的查询语句,两次读到的结果不一样。

5.3 可重复读(repeatable read)

MySQL 默认隔离级别,解决了脏读和不可重复读,同时通过 Next-Key 锁(间隙锁 + 行锁)解决了幻读问题。

5.4串行化(serializable)

最高的隔离级别,强制所有事务串行执行,完全解决了脏读、不可重复读、幻读,但并发性能极差,生产环境几乎不使用。

核心特点:所有读写操作都会加锁,事务只能串行执行,安全性最高,但并发性能最低。


六. 一致性:事务的最终归宿

我们前面讲了原子性、隔离性、持久性,最终都是为了保证一致性。

一致性分为两个层面:

  • 数据库层面的一致性:事务执行前后,数据库的完整性约束(主键、外键、唯一索引、非空约束等)不会被破坏,比如主键不会重复、库存不会出现负数。
  • 业务层面的一致性:这是由我们的业务代码决定的,比如转账前后两个账户的总金额不变、订单创建后库存必须扣减、用户下单后优惠券必须标记为已使用。

MySQL 的事务机制,为我们提供了保证一致性的技术基础(原子性、隔离性、持久性),但最终的业务一致性,还是需要我们的业务代码来保证。一句话总结:通过 AID(原子性、隔离性、持久性)来保证 C(一致性)。


七、 硬核底层:MVCC(多版本并发控制)原理

在并发访问中,最常见的场景有三种:读-读、读-写、写-写。

  • 读-读:不需要任何并发控制。

  • 写-写:通过加锁解决,防止更新丢失。

  • 读-写:如果读写互相阻塞,性能会极差(类似 Serializable)。为了实现读写不冲突、无锁并发,InnoDB 引入了MVCC(Multi-Version Concurrency Control,多版本并发控制)。

1. 理解 MVCC 的三个核心基石

要搞懂 MVCC 的秘密,必须先掌握以下三个前提:

  1. 3个隐式字段

  2. Undo Log(回滚日志)

  3. Read View(一致性读视图)

核心基石一:3 个记录隐藏字段

InnoDB 在存储表记录时,除了我们定义的显式列之外,每一行数据都会自动添加 3 个隐藏列:

  • DB_TRX_ID(6 字节):最近修改或插入当前记录的事务 ID。

  • DB_ROLL_PTR(7 字节):回滚指针,指向这条记录在undo log中的上一个历史版本地址。

  • DB_ROW_ID(6 字节):隐式自增主键。如果表没有定义主键,InnoDB 会自动使用该字段构建聚簇索引。

  • (另外还有一个删除标记 flag 列,表示记录是否被逻辑删除。)

假设我们在student表插入一条初始记录('张三', 28),它的结构大概如下:

name

age

DB_TRX_ID

DB_ROW_ID

DB_ROLL_PTR

张三

28

null (假设初始为0)

1

null

核心基石二:Undo Log 与历史版本链

Undo Log是 MySQL 存放在内存缓冲区(并在适当时机刷盘)的一段日志。 每当一个事务修改数据时,InnoDB 会采用写时拷贝(Copy-on-Write)的机制:

  1. 修改前,先给该行加锁。

  2. 将当前记录复制一份到undo log中。

  3. 修改当前记录的值,并将DB_TRX_ID改为当前事务的 ID。

  4. 将当前记录的DB_ROLL_PTR指向刚刚复制到undo log中的旧版本地址。

  5. 释放锁。

【模拟演练】版本链的形成过程:
  • 第一步:事务 10将name从“张三”改为“李四”: 此时,最新物理记录变为“李四”,而它的回滚指针指向了undo log中的“张三”。

  • 第二步:事务 11将age从 28 改为 38: 物理记录变为('李四', 38),回滚指针指向undo log中的('李四', 28),而('李四', 28)再次指向('张三', 28)。

这样,一个以物理最新记录为头节点、通过DB_ROLL_PTR指针不断向前溯源的历史版本链(链表)就形成了!

[最新物理数据: 李四, 38] (DB_TRX_ID = 11) | v (DB_ROLL_PTR) [Undo Log 历史版本 1: 李四, 28] (DB_TRX_ID = 10) | v (DB_ROLL_PTR) [Undo Log 历史版本 2: 张三, 28] (DB_TRX_ID = 0/null)

读的类型区分:

  • 当前读:读取最新版本的记录,需要加锁。如insert、update、delete,以及select... lock in share mode、select ... for update。

  • 快照读:读取历史版本(不加锁),避免与写操作冲突。这就是 MVCC 的精髓。

核心基石三:Read View 的可见性算法

那么问题来了:面对这一条长长的历史版本链,当某个事务执行快照读(SELECT)时,它到底应该读取链表中的哪一个版本? 答案是:由Read View决定。

当事务执行快照读时,MySQL 会自动生成一个Read View读视图。在源码中,它是一个用来进行可见性判断的类,主要包含以下核心属性:

  • m_ids:在生成 Read View 的那一刻,系统内当前活跃且未提交的事务 ID 列表。

  • up_limit_id:m_ids列表中事务 ID最小的那个值(低水位:比这个 ID 小的事务,说明在创建 Read View 之前已经全部提交,肯定可见)。

  • low_limit_id:目前已出现过的最大事务 ID + 1,即尚未分配的下一个事务 ID(高水位:比这个 ID 大的事务,说明是在创建 Read View 之后才开启的,绝对不可见)。

  • creator_trx_id:创建该 Read View 的当前事务 ID(自己修改的数据永远可见)。

可见性判断算法图解:

当事务尝试读取版本链上的某条记录(其事务 ID 为 DB_TRX_ID)时,判断逻辑如下:

low_limit_id up_limit_id | | v -----+--------------+-----------------------------> 时间轴 (事务ID递增) 历史已提交事务 | 活跃未提交事务 (m_ids) | 快照后新开启的事务 (可见) | (不可见/判断可见) | (不可见)

对应源码策略:

2. 探究:RC 与 RR 的本质区别是什么?

很多人会问:既然 RC(读已提交)和 RR(可重复读)都使用了 MVCC,那为什么它们读取的结果会不同?

核心本质只有一个:Read View 的生成时机不同!

  • RC(读提交)级别: 在事务中,每次执行SELECT(快照读)时,都会重新生成最新的 Read View。 因为每次重新生成,那些在两次SELECT之间刚刚提交的事务,其 ID 就会从新Read View的m_ids中被移除,因此后面那次读取就能看到新的提交。这就是不可重复读的来源。

  • RR(可重复读)级别: 在一个事务中,只有在第一次执行SELECT(快照读)时才会创建 Read View。 此后在整个事务的生命周期内,不管你执行多少次SELECT,读取使用的都是同一个 Read View!因此不管别的并发事务怎么提交,它读取到的数据永远跟第一次读取时一模一样。

实例对比(测试用例):
测试用例 1 (在 RR 隔离级别下):
  1. 事务 A与事务 B同时begin。

  2. 事务 B首次执行:select * from user;(此时生成 Read View)。

  3. 事务 A执行更新:update user set age = 18 where id = 1;并commit。

  4. 事务 B再次执行快照读:select * from user;。

    • 结果:依然看到老数据,看不到age = 18。

  5. 事务 B执行当前读:select * from user lock in share mode;。

    • 结果:因为当前读强制读取最新物理版本并加锁,所以看到了age = 18。

测试用例 2 (在 RR 隔离级别下):
  1. 事务 A与事务 B同时begin。

  2. 事务 B在事务 A 更新前,没有进行任何快照读。

  3. 事务 A执行更新:update user set age = 28 where id = 1;并commit。

  4. 事务 B此时才执行首次快照读:select * from user;(此时才生成 Read View)。

    • 结果:读到了最新的age = 28!

    • 分析:因为事务 B 首次快照读是在 A 提交之后,所以新生成的 Read View 判定 A 已经提交,因此可见。

这充分证明:RR 级别下的快照读,其数据版本极度依赖于该事务中首次执行快照读的时机!


结语

MySQL 的事务机制是整个关系型数据库高并发设计的精髓所在:

  • 通过Undo Log,MySQL 实现了低成本的“版本保留”与“事务回滚”(保证原子性与持久性)。

  • 通过Read View配合可见性算法,MySQL 实现了读写不冲突,极大地释放了并发潜能。

  • RC 与 RR 的本质不同仅仅是 Read View 产生频次的不同。

深刻理解这些底层机制,不仅能帮助你在面试中对答如流、尽显深度,更能在实际高并发系统设计中,让你对数据的行为有着绝对的掌控力!

如果你觉得这篇文章写得足够硬核、对你有帮助,欢迎点赞、收藏并关注博主!我们下期再见!

相关新闻

  • OpCore Simplify:10分钟完成黑苹果OpenCore EFI配置的终极指南
  • 2026年7月Agent开发面试题 -- 高阶篇
  • Kimi LeetCode 3459. 最长 V 形对角线段的长度 C语言实现

最新新闻

  • 为什么选择Genome?探索Swift中失败驱动映射的完整解决方案
  • 10个实用p5示例教程:让你的Python代码动起来
  • 5个核心技巧:使用BeeRef高效管理视觉参考素材的完整指南
  • 火山引擎Coding Plan抢购难?开发者API调用成本控制与多模型切换实战指南
  • NVMeFix常见问题解决:VMware和Samsung PM981死机修复终极指南
  • ContEx与Phoenix LiveView集成:实时数据可视化最佳实践指南

日新闻

  • STM32F745VG与MC6470 IMU的高性能姿态控制系统设计
  • 机器不消费,人何以生存
  • AI项目操作手册编写规范与最佳实践

周新闻

  • Windows字体自定义终极方案:No!! MeiryoUI完全指南
  • Deepin Boot Maker:告别命令行,3分钟制作Linux启动盘的智能解决方案
  • Plain Craft Launcher 2:重新定义你的Minecraft游戏体验

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号