MySQL基础篇之事务

我们常说磁盘 / 内存的管理颗粒是 "块 / 页",那 MySQL 数据库的执行与管理颗粒是什么?答案是:事务(Transaction)

在没有学习事务之前,你可以认为之前的每一条单句 SQL 都自成一个事务。而事务的本质,是一组逻辑上不可分割的 SQL 语句集合 ------ 要么全部执行成功,要么全部失败回滚,是数据库保证数据可靠性、并发正确性的核心机制。

事务的四大核心特性,简称 ACID

  1. 原子性(Atomicity)
  2. 一致性(Consistency)
  3. 隔离性(Isolation)
  4. 持久性(Durability)

前置说明:MySQL 中只有 InnoDB 存储引擎 完整支持事务,MyISAM 不支持事务,仅擅长只读查询场景。因此下文所有讲解均基于 InnoDB 引擎。


前置:统一测试表

下文所有演示均基于这张学生表:

复制代码
CREATE TABLE `student` (
  `id` int NOT NULL AUTO_INCREMENT,
  `ename` varchar(10) NOT NULL,
  `age` int NOT NULL DEFAULT '18',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;

一、原子性(Atomicity)

1.1 核心概念

原子性指一个事务是不可分割的最小执行单元,最终只会呈现两种状态:要么全部执行完成,要么完全没有执行,不会存在 "执行了一半" 的中间状态。

1.2 自动提交(autocommit)

自动提交是 MySQL 的默认行为,也是理解事务的前提。

  • 查看自动提交状态:

    复制代码
    SHOW VARIABLES LIKE 'autocommit';
  • 设置自动提交状态:

    复制代码
    SET autocommit = OFF; -- 关闭自动提交,0 等价于 OFF
    SET autocommit = ON;  -- 开启自动提交,1 等价于 ON
核心规则
  • autocommit = ON(默认):每一条 SQL 语句都会被自动包裹成一个独立事务,语句执行完自动提交。对用户来说,几乎感知不到事务的存在。
  • autocommit = OFF :当前会话会隐式开启一个事务,所有 SQL 语句都在这个事务内,直到用户手动执行 COMMIT 提交或 ROLLBACK 回滚。
  • 手动开启事务(START TRANSACTION / BEGIN :一旦手动执行 BEGINSTART TRANSACTION,无论 autocommit 是开是关,都会开启一个新的显式事务,必须手动提交 / 回滚,不再触发自动提交。

1.3 事务基础操作与保存点

复制代码
-- 开启一个显式事务
START TRANSACTION; -- 等价于 BEGIN;

-- 执行DML操作
INSERT INTO student (ename, age) VALUES ('孙权', 19), ('吕布', 20);

-- 设置保存点 save1
SAVEPOINT save1;

INSERT INTO student (ename, age) VALUES ('刘备', 19), ('刘禅', 20);

SAVEPOINT save2;

INSERT INTO student (ename, age) VALUES ('赵云', 19), ('张飞', 20);

SAVEPOINT save3;

此时查询可以看到 6 条记录。

部分回滚(回滚到保存点)
复制代码
ROLLBACK TO save2;

执行后,save2 之后的操作(赵云、张飞的插入)全部撤销,表中只剩下 4 条记录。

  • 保存点是事务内部的 "进度锚点",允许事务内的部分回滚,不破坏事务的原子性;
  • 保存点只能在当前事务内使用,事务提交或回滚后自动失效。
全部回滚
复制代码
ROLLBACK;

直接回滚整个事务,事务所做的所有修改全部撤销,事务结束,数据回到事务开始前的状态。

提交事务
复制代码
COMMIT;

提交后,事务所做的所有修改永久生效,事务结束。

1.4 原子性的本质

原子性的核心保障是:事务执行过程中发生任何异常(MySQL 崩溃、操作系统崩溃、断电),未提交的事务都会被全部回滚,不会留下半完成的数据。最终事务只有两种结果:提交成功(全部生效)、回滚 / 失败(全部不生效)。


二、持久性(Durability)

2.1 核心概念

持久性指事务一旦提交,其对数据库的修改就是永久的,后续发生任何故障(系统崩溃、断电)都不会丢失已提交的数据。

2.2 实现基础:Redo Log(重做日志)

很多初学者会混淆 redo log 和 undo log,这里明确区分:

  • Undo Log(回滚日志):记录数据修改前的版本,用于事务回滚和 MVCC 多版本并发控制;
  • Redo Log(重做日志):记录数据修改后的操作,用于崩溃恢复,保证持久性。

事务执行 DML 时,不是直接修改磁盘上的数据文件,而是先写入 Redo Log Buffer,再按策略刷到磁盘。当数据库崩溃重启时,通过 redo log 把已提交但还没刷到数据文件的操作重做一遍,保证数据不丢。

2.3 刷盘策略:innodb_flush_log_at_trx_commit

该参数控制事务提交时 redo log 的刷盘行为,直接决定了持久性与性能的权衡:

参数值 刷盘时机 数据丢失风险 性能 ACID 符合度
1(默认) 每次事务提交时,立即将 redo log buffer 刷入磁盘,并执行 fsync 落盘 极低(仅磁盘硬件故障可能丢数据) 最差 严格满足持久性
0 每秒由后台线程统一刷盘,事务提交不触发刷盘 MySQL 进程崩溃时,丢失约 1 秒内已提交的事务 最好 不满足
2 事务提交时写到操作系统 Page Cache,每秒刷盘 MySQL 进程崩溃不丢;操作系统崩溃 / 断电时,丢失约 1 秒数据 中等 不严格满足

生产环境如果要求严格的 ACID 持久性,必须保持默认值 1;追求极致性能可酌情调整,但需接受对应的数据丢失风险。


三、隔离性(Isolation)

隔离性是事务最复杂、也是考点最多的部分,决定了并发事务之间的数据可见性。

3.1 核心概念

隔离性指多个并发事务同时读写同一数据时,保证数据不被干扰的特性。

并发操作的三种类型
  • 读读并发:仅读取数据,不做修改,不存在数据安全问题,可完全并行。
  • 写写并发 :修改同一行数据时,通过行锁串行化;不同行的写操作可以并行,并非所有写操作都串行。
  • 读写并发:读和写同时进行时的可见性问题,是隔离性讨论的核心。

3.2 四个隔离级别

SQL 标准定义了四个隔离级别,从低到高分别是:读未提交、读提交、可重复读、串行化。级别越高,隔离性越强,并发性能越差。

查看隔离级别
复制代码
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;

-- 查看当前会话隔离级别
SELECT @@session.transaction_isolation;
修改隔离级别
复制代码
-- 修改当前会话隔离级别(仅对当前会话生效)
SET SESSION TRANSACTION ISOLATION LEVEL 级别名称;

-- 修改全局隔离级别(对后续新会话生效,当前会话不生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL 级别名称;

补充:MySQL 服务启动时会读取配置文件中的全局参数;修改全局参数后,已有会话不会更新,新会话才会继承;重启服务后全局参数会重置为配置文件中的值。


级别一:读未提交(READ UNCOMMITTED,RU)
特性

一个事务还没提交,它做的修改就能被其他事务看到。

现象:脏读

读到了其他事务未提交的 "脏数据",称为脏读。

演示

事务 A:

复制代码
BEGIN;
INSERT INTO student (ename, age) VALUES ('诸葛亮', 39), ('典韦', 50);
-- 不提交

事务 B(RU 级别):

复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
SELECT * FROM student WHERE ename IN ('诸葛亮', '典韦');

结果:事务 B 可以查到这两条未提交的数据。

评价

隔离级别最低,并发最高,但脏读问题严重,生产业务中几乎不用。


级别二:读提交(READ COMMITTED,RC)
特性

一个事务只能看到其他事务已经提交的修改;未提交的修改对其他事务不可见。

解决的问题:脏读
存在的问题:不可重复读

同一个事务内,两次读取同一行数据,中间有其他事务提交了修改,两次读到的结果不一样。

演示

事务 A:

复制代码
BEGIN;
INSERT INTO student (ename, age) VALUES ('孙策', 19), ('孙膑', 20);
-- 不提交

事务 B(RC 级别):

复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT * FROM student WHERE ename IN ('孙策', '孙膑'); -- 查不到

此时事务 A 提交:

复制代码
COMMIT;

事务 B 再次查询:

复制代码
SELECT * FROM student WHERE ename IN ('孙策', '孙膑'); -- 可以查到

同一个事务内,两次同样的查询结果不一致,就是 "不可重复读"。


级别三:可重复读(REPEATABLE READ,RR)
特性

MySQL InnoDB 的默认隔离级别。同一个事务内,多次读取同一行数据的结果始终一致,不受其他事务提交的影响。

解决的问题:脏读、不可重复读
存在的问题:幻读(InnoDB 通过 Next-Key Lock 很大程度上解决了幻读)

其他事务在该范围内插入了新行,当前事务再次查询时就会看到这些新增行,从而导致查询结果行数发生变化,

演示

事务 A:

复制代码
BEGIN;
INSERT INTO student (ename, age) VALUES ('孙策', 19), ('孙膑', 20);
COMMIT;

事务 B(RR 级别):

复制代码
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM student WHERE ename IN ('孙策', '孙膑'); -- 查不到

即使事务 A 已经提交,事务 B 再次查询,依然查不到数据。


级别四:串行化(SERIALIZABLE)
特性

最高隔离级别,所有事务串行执行,不允许并发。

解决的问题:脏读、不可重复读、幻读
效果

所有读操作都会加共享锁,写操作加排他锁,读写、写写都互斥,完全串行。并发性能极差,仅在对数据一致性要求极高的场景使用。

3.3 隔离级别与问题对应表

隔离级别 脏读 不可重复读 幻读 并发性能
读未提交 RU ✅ 存在 ✅ 存在 ✅ 存在 最高
读提交 RC ❌ 解决 ✅ 存在 ✅ 存在
可重复读 RR ❌ 解决 ❌ 解决 部分解决 中等
串行化 Serializable ❌ 解决 ❌ 解决 ❌ 解决 最低

四、一致性(Consistency)

4.1 核心概念

一致性指事务执行前后,数据库始终保持数据的正确性与完整性 ------ 既符合数据库层面的约束定义(主键、唯一键、外键、数据类型),也符合业务逻辑的预期。

4.2 一致性与其他三大特性的关系

ACID 中,一致性是最终目的,原子性、隔离性、持久性是实现一致性的手段

  • 原子性保证事务没有中间状态,不会破坏数据完整性;
  • 隔离性保证并发操作不会互相干扰,不会产生数据异常;
  • 持久性保证提交后的数据不会丢失。

三者共同保障了数据库始终处于一致的状态。但一致性同时也依赖于业务逻辑的正确性 ------ 如果事务本身的业务逻辑错误(比如给工资扣成负数),数据库层面无法保证业务一致性。


五、深入隔离性:MVCC 底层原理

隔离性的核心实现机制是 MVCC(多版本并发控制)。InnoDB 通过 undo log 保留数据的历史版本,配合 ReadView 实现 "快照读",让读写操作不用加锁也能保证隔离,大幅提升并发性能。

5.1 Undo Log 与版本链

回滚的两种实现思路
  • 方案一:记录反向操作(比如 INSERT 对应 DELETE),回滚时执行反向操作。缺点是修改复杂时开销大。
  • 方案二:保留数据的历史版本(快照),回滚时直接恢复旧版本。InnoDB 采用的就是这种方案,通过 undo log 存储历史版本。

5.2 行的隐藏字段

InnoDB 每行数据除了用户定义的显式字段,还包含 3 个隐式字段和删除标记:

隐式字段 字节数 作用
DB_TRX_ID 6 字节 最后一次修改(插入 / 更新)该行的事务 ID
DB_ROLL_PTR 7 字节 回滚指针,指向该行上一个版本在 undo log 中的位置
DB_ROW_ID 6 字节 隐藏主键。当表没有定义主键时,InnoDB 自动生成该字段作为聚簇索引;有主键则不存在此字段

另外还有删除标记位:标识该行是否被删除。删除操作本质是打标记,不是物理删除,既高效又支持回滚,和文件系统的数据块删除逻辑类似。

这也解释了一个常见问题:为什么表没有主键时,全表扫描会很慢?因为没有显式主键时,查询条件不会命中隐藏的聚簇索引,只能全表扫描。

5.3 版本链的形成

每当对一行数据做修改时,都会把旧版本写入 undo log,然后通过 DB_ROLL_PTR 形成一条从最新版本指向历史版本的链式结构,称为版本链

示例

初始插入一行(事务 ID=1):

id ename age DB_TRX_ID DB_ROLL_PTR
19 张三 20 1 NULL

执行更新 UPDATE student SET age = 30 WHERE ename = '张三'(事务 ID=1):

  1. 原来的行作为旧版本写入 undo log,地址为 0xaa;
  2. 当前行修改为新值,更新 DB_ROLL_PTR 指向旧版本地址。

新版本行:

id ename age DB_TRX_ID DB_ROLL_PTR
19 张三 30 1 0xaa

版本链的每个节点,都对应一次修改的历史快照;事务回滚时,沿着指针找回旧版本即可。

5.4 快照读与 ReadView

什么是快照

快照,字面意思就是 "快速拍摄的照片"。

想象你用手机给一个正在变化的场景拍了一张照片 ------ 按下快门的那一刻,画面就被定格了。之后场景再怎么变化,照片里的内容都不会变。

数据库中的快照也是同一个道理:在某个时间点,给数据库中的数据 "拍一张照片",把那一刻的数据状态完整保存下来。之后不管数据怎么被修改、被其他事务提交,这张 "照片" 里的内容始终不变。

什么是快照读?

我们平时执行的普通 SELECT 就是快照读:不加锁,直接读取数据的可见版本,基于 ReadView 判断哪个版本可见。

与之对应的是当前读SELECT ... FOR UPDATEINSERTUPDATEDELETE,读取最新版本并加锁。

ReadView 结构

ReadView 是事务执行快照读时生成的 "读视图",记录了生成视图那一刻系统中活跃事务的状态,用来判断版本链上的哪个版本对当前事务可见。

核心结构:

复制代码
class ReadView {
private:
    trx_id_t m_up_limit_id;    // 低水位:活跃事务中最小的事务ID
    trx_id_t m_low_limit_id;   // 高水位:系统下一个待分配的事务ID(当前最大事务ID+1)
    trx_id_t m_creator_trx_id; // 创建该ReadView的事务ID
    ids_t m_ids;              // 生成ReadView时,所有活跃(未提交)事务的ID列表
    // ...
};

各字段含义:

  1. m_ids:生成 ReadView 的瞬间,系统中所有正在活跃、未提交的事务 ID 列表(不包含当前事务自己)。
  2. m_up_limit_id(低水位)m_ids 列表中最小的事务 ID。
  3. m_low_limit_id(高水位):生成 ReadView 时,系统尚未分配的下一个事务 ID,也就是当前已出现过的最大事务 ID + 1。
  4. m_creator_trx_id:创建当前 ReadView 的事务自身的 ID。

5.5 可见性判断规则

对于版本链上的某一个版本(DB_TRX_ID 为修改该版本的事务 ID),按以下规则判断是否可见:

  1. DB_TRX_ID < m_up_limit_id 该版本的修改事务,在 ReadView 生成前就已经提交了,可见
  2. DB_TRX_ID >= m_low_limit_id 该版本的修改事务,是在 ReadView 生成之后才开启的,不可见
  3. DB_TRX_ID 在两者之间
    • 如果 DB_TRX_IDm_ids 列表中:说明该事务在 ReadView 生成时还活跃未提交,不可见
    • 如果 DB_TRX_ID 不在 m_ids 列表中:说明该事务在 ReadView 生成时已经提交,可见

如果当前版本不可见,就沿着 DB_ROLL_PTR 找上一个版本,重复判断,直到找到可见的版本。

5.6 RC 与 RR 的本质区别

RC 和 RR 隔离级别最核心的区别,就是 ReadView 的生成时机不同

1. 读提交(RC)

每次执行快照读(SELECT),都会重新生成一个新的 ReadView。 所以每次查询都能看到最新提交的事务修改,因此会出现不可重复读。

2. 可重复读(RR)

只在事务中第一次执行快照读时,生成一个 ReadView,后续所有快照读都复用这个 ReadView。 所以整个事务内看到的数据版本都是一致的,不受其他事务提交的影响,实现了可重复读。

举例理解

假设系统中有事务 2、3、4、5 同时活跃:

  • 事务 3 先提交;
  • 事务 4 执行 SELECT。
  • RC 级别:事务 4 每次 SELECT 都生成新的 ReadView。事务 3 提交后,新的 ReadView 的 m_ids 里就没有 3 了,所以能看到事务 3 的修改。
  • RR 级别:事务 4 第一次 SELECT 生成 ReadView 后就不再变化。即使事务 3 提交了,m_ids 列表也不变,所以还是看不到事务 3 的修改。
相关推荐
李白客1 小时前
数据库管理工具怎么选?Navicat、DataGrip、DBeaver 六维横向对比与选型建议
运维·数据库
马立杰1 小时前
mysql中Illegal mix of collations for operation “UNION”错误的解决方法
数据库·sql·网络安全
蓝速科技1 小时前
蓝速 K10 三防平板在工业恶劣工况下的选型与应用指南
大数据·运维·数据库·人工智能·科技
bksczm1 小时前
MySQL基础篇之视图与用户管理
linux·数据库·sql·mysql
香菜TTT1 小时前
怎么实现MySQL的主从同步机制
数据库·mysql
Ahtacca1 小时前
Linux 基础实验:从终端操作到 C 语言编译
linux·运维·运维开发·虚拟机
邪修king1 小时前
Re:Linux 系统篇(二十九):动静态库Chapter2:动态库深度辨析 —— 核心本质、制作流程、双阶段查找模型与排错指南
android·java·linux·开发语言
云泽8083 小时前
从零搭建 WSL2 + Ubuntu C/C++ 开发环境:原理、实践与底层架构解析
linux·c++·windows·ubuntu·wsl2
2501_933670799 小时前
2026秋招数据分析岗备考路线:SQL、BI、项目与面试题拆解
数据库