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

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

🔥个人主页: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. 只要输入了beginstart 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. 释放锁。

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

  • 第二步:事务 11age从 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)

读的类型区分

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

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

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

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

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

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

  • up_limit_idm_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 Viewm_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 产生频次的不同

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

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

http://www.jsqmd.com/news/1123753/

相关文章:

  • 2026年7月Agent开发面试题 -- 高阶篇
  • 2026年7月全球企业小程序开发工具测评:含零代码SAAS、AI编程、源码定制
  • 2025微信小程序反编译终极指南:如何用unveilr快速提取小程序源码
  • Kimi LeetCode 3459. 最长 V 形对角线段的长度 C语言实现
  • 终极UE4SS实战指南:如何无需源码深度定制Unreal Engine游戏
  • FORCE_PROMPT_CACHING_5M,Claude Code 缓存 TTL 的刹车踏板
  • 5个实用的Google Cloud Vision API示例项目详解
  • 个人分享|校园新闻网站源码与配套论文,课设毕设参考素材!
  • 黑苹果配置革命:OpCore Simplify - 自动化EFI生成终极解决方案
  • CTF Web安全入门:三个月系统学习路线与实战技巧
  • 解决Obsidian中嵌入Claude Code的问题
  • ICM-42688-P与PIC18LF27K42在工业振动监测中的优化应用
  • Lua 5.1字节码反编译终极指南:luadec51完整使用教程
  • 3. 应用编程---信号
  • 大模型能力对比:基于场景锚点的AI选型方法论
  • OpenCore Legacy Patcher完整指南:让老款Mac免费升级最新macOS的终极方案
  • Deepin Boot Maker终极指南:3步制作Linux启动盘的最佳实践
  • 林伽一 · AI科技日报 |LongCat-2.0宣称中国芯片突破,Claude Sonnet 5自报分数解析
  • ComfyUI-WanVideoWrapper实现AI视频生成性能突破:径向注意力与FP8量化技术深度解析
  • 终极指南:3分钟学会用FanControl掌控Windows电脑风扇,告别噪音烦恼
  • “写了十年代码,我才懂什么叫“一即一切“:分形几何×七境修心,一个程序员的自救指南
  • Linux高并发Reactor反应堆模式深度精讲,单Reactor、多Reactor架构、epoll高并发服务器手写、Nginx核心架构落地实战
  • Python cryptography库实战:RSA非对称加密与数字签名完整指南
  • 3分钟掌握Diablo Edit2:暗黑2存档修改器的终极解决方案
  • The Other Side of the Grail: Risks to the Mission System and the Complete Solution
  • 赋值操作符:=和复合赋值
  • 2026图片去水印怎么弄?无痕去水印实用技巧+免费工具手机电脑教程
  • 用 AI 写代码做家庭调酒小程序:真正难的是把酒库到保存跑通
  • ClaudeMax实战压测:什么场景下它才不可替代?
  • 质量门脚本:用Python给AI输出加上自动质检(附完整源码)