【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; }并发冲突场景:
客户端 A检查数据库,发现
nums = 1 > 0,决定卖票(步骤 1)。在 A 还没有来得及更新数据库时,客户端 B也进来检查,发现
nums依然是 1,也决定卖票(步骤 2、3)。A 执行更新,将
nums改为 0(步骤 4)。B 也执行更新,将
nums改为 0(步骤 5)。
结果:同一张票被卖了两次(超卖问题)!
如何解决上述问题?
要彻底解决这种并发混乱,购票操作必须满足以下属性:
原子性:买票的过程必须是一个整体,不能停在中间。
隔离性:客户端之间的买票操作互相不应该有干扰。
持久性:买票成功后,数据的修改必须是永久有效的。
一致性:买票前和买票后,数据库的状态都必须是确定的、符合业务规则的。
这,正是事务(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 也会直接在后台将其回滚。
终极结论:
只要输入了
begin或start transaction,事务就必须通过commit提交才会持久化,不受全局autocommit变量的影响。MySQL 的单条 DML 语句在 InnoDB 下默认都是当成独立事务自动提交的。
四、 深入探究:事务隔离级别与并发问题
1. 为什么要有隔离性?
MySQL 作为一个高性能的网络服务,随时可能有成百上千个客户端发起并发事务。如果大家同时修改同一张表、同一行数据,在不加保护的前提下必然会乱套。
为了在高并发性能与数据安全性之间取得平衡,MySQL 引入了隔离性,并提供了四种不同的隔离级别。
2. 四大隔离级别与 anomalies(并发缺陷)
读未提交(Read Uncommitted, RU):
特点:没有任何真正的隔离保护,效率最高,但基本不加锁。
致命缺陷:存在脏读(Dirty Read)。即一个事务在运行中,能读取到另一个事务更新了、但尚未提交(Uncommitted)的数据。如果另一个事务最后回滚了,那读取到的就是虚假数据。
读提交(Read Committed, RC):
特点:一个事务只能看到其他事务已经提交(Committed)的修改。这也是绝大多数数据库(如 Oracle)默认的隔离级别。
缺陷:存在不可重复读(Non-repeatable Read)。在同一个事务内,多次执行相同的
SELECT查询,可能会因为其他事务在此期间提交了修改,导致前后读取到的数据内容不一致。
可重复读(Repeatable Read, RR):
特点:MySQL 的默认隔离级别。它确保在同一个事务内,多次读取同一行数据,看到的结果始终是一致的。
理论缺陷:存在幻读(Phantom Read)问题。即当事务 A 进行范围查询时,事务 B 在该范围内插入(Insert)了新记录并提交,事务 A 再次读取该范围时,会莫名其妙多出一些记录,就像产生了幻觉。
注意:MySQL 的 InnoDB 引擎在 RR 级别下,通过Next-Key Locks(GAP 间隙锁 + 行锁)已经很大程度上解决了幻读问题!
串行化(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 的秘密,必须先掌握以下三个前提:
3个隐式字段
Undo Log(回滚日志)
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)的机制:
修改前,先给该行加锁。
将当前记录复制一份到
undo log中。修改当前记录的值,并将
DB_TRX_ID改为当前事务的 ID。将当前记录的
DB_ROLL_PTR指向刚刚复制到undo log中的旧版本地址。释放锁。
【模拟演练】版本链的形成过程:
第一步:事务 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 隔离级别下):
事务 A与事务 B同时
begin。事务 B首次执行:
select * from user;(此时生成 Read View)。事务 A执行更新:
update user set age = 18 where id = 1;并commit。事务 B再次执行快照读:
select * from user;。结果:依然看到老数据,看不到
age = 18。
事务 B执行当前读:
select * from user lock in share mode;。结果:因为当前读强制读取最新物理版本并加锁,所以看到了
age = 18。
测试用例 2 (在 RR 隔离级别下):
事务 A与事务 B同时
begin。事务 B在事务 A 更新前,没有进行任何快照读。
事务 A执行更新:
update user set age = 28 where id = 1;并commit。事务 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 产生频次的不同。
深刻理解这些底层机制,不仅能帮助你在面试中对答如流、尽显深度,更能在实际高并发系统设计中,让你对数据的行为有着绝对的掌控力!
如果你觉得这篇文章写得足够硬核、对你有帮助,欢迎点赞、收藏并关注博主!我们下期再见!