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) :一旦手动执行 BEGIN 或 START 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 UPDATE、INSERT、UPDATE、DELETE,读取最新版本并加锁。

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_ID 在 m_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 的修改。
相关推荐
顺风尿一寸4 小时前
手写迷你 Docker:用 Namespace + Cgroup v2 实现一个简单容器
linux
一木 之林5 小时前
DeepSeek Agent 开发(三)
java·大数据·linux
ggaofeng5 小时前
自己编写邮件服务器和客户端
数据库
java1234_小锋5 小时前
【技术专题】Mysql8 数据库 - Mysql8 添加,更新,删除数据
数据库·mysql
PaperData5 小时前
2015-2024年各省绿色贸易数据
数据库
小羊没烦恼!5 小时前
系统内部模块(子系统)之间的耦合以及模块(子系统)划分
java·开发语言·前端·数据库·算法·c#
大猫会长6 小时前
调用codebuddy集成的supabase授权白屏解决方法
linux·运维·服务器
羔羊++6 小时前
29_实验二十八_buildroot配置与编译
linux
麦壳饼6 小时前
深入探讨:SonnetDB 的文件格式与存储布局
数据库·sonnetdb
LoneEon6 小时前
CentOS7 部署 Nacos3.x 集群实战:从注册中心到 AI 管理中心
linux·人工智能·nacos