我们常说磁盘 / 内存的管理颗粒是 “块 / 页”,那 MySQL 数据库的执行与管理颗粒是什么?答案是:事务(Transaction)。
在没有学习事务之前,你可以认为之前的每一条单句 SQL 都自成一个事务。而事务的本质,是一组逻辑上不可分割的 SQL 语句集合 —— 要么全部执行成功,要么全部失败回滚,是数据库保证数据可靠性、并发正确性的核心机制。
事务的四大核心特性,简称 ACID:
前置说明: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 的刷盘行为,直接决定了持久性与性能的权衡:
| 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):
| 19 | 张三 | 20 | 1 | NULL |
执行更新 UPDATE student SET age = 30 WHERE ename = '张三'(事务 ID=1):
新版本行:
| 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列表
// …
};
各字段含义:
5.5 可见性判断规则
对于版本链上的某一个版本(DB_TRX_ID 为修改该版本的事务 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 的修改。
网硕互联帮助中心







评论前必须登录!
注册