2026年7月11日 · 5 分钟阅读
MySQL 事务原理:ACID、隔离级别、Undo Log、Read View 与 MVCC
梳理 MySQL 事务四大特性、并发问题、隔离级别、LBCC、MVCC、Undo Log、Read View、当前读和快照读。
事务是数据库最重要的机制之一。它保证一组 SQL 要么都成功,要么都失败,同时还要在多事务并发执行时尽量兼顾正确性和性能。
MySQL 事务真正难的地方,不是记住 ACID,而是理解隔离性为什么会被分级、MVCC 为什么能做到读写不冲突。
什么是事务
事务是逻辑上的一组操作。组成这组操作的 SQL,要么全部成功,要么全部失败。
例如转账:
update account set balance = balance - 100 where id = 1;
update account set balance = balance + 100 where id = 2;
这两条 SQL 必须作为一个整体执行。不能扣款成功、加款失败。
ACID
事务有四大特性:
| 特性 | 含义 |
|---|---|
| Atomicity 原子性 | 事务内操作要么全部成功,要么全部失败 |
| Consistency 一致性 | 事务前后数据满足业务约束 |
| Isolation 隔离性 | 多个事务并发时互不干扰到约定程度 |
| Durability 持久性 | 事务提交后数据永久保存 |
原子性通常依赖 undo log,持久性依赖 redo log,隔离性依赖锁和 MVCC。
事务并发问题
多个事务同时访问相同数据时,可能出现几类问题:
| 问题 | 说明 |
|---|---|
| 脏读 | 读到其他事务未提交的数据 |
| 不可重复读 | 同一事务内两次读取同一行,结果不同 |
| 幻读 | 同一事务内两次范围查询,记录数量不同 |
| 丢失更新 | 多个事务同时更新,后提交覆盖先提交 |
隔离级别就是在性能和隔离性之间做取舍。
隔离级别
SQL 标准定义了四种隔离级别:
| 隔离级别 | 能解决的问题 |
|---|---|
| Read Uncommitted | 基本不解决,可能脏读 |
| Read Committed | 解决脏读 |
| Repeatable Read | 解决脏读、不可重复读,InnoDB 默认级别 |
| Serializable | 串行执行,隔离最强,性能最低 |
隔离性越强,并发能力通常越弱。数据库不能简单让所有事务排队执行,否则性能会很差。
LBCC:基于锁的并发控制
LBCC 是 Lock Based Concurrency Control,基于锁的并发控制。
思路很直接:读写冲突时加锁,让事务排队。
优点是规则容易理解,缺点是读写互相阻塞,并发性能受影响。
MVCC:多版本并发控制
MVCC 是 Multi-Version Concurrency Control,多版本并发控制。
它的核心思想是:同一行数据可以存在多个版本,不同事务根据自己的视图读取不同版本。
这样普通 SELECT 可以不加锁,读写不冲突,大幅提升并发能力。
MVCC 依赖两个关键机制:
- Undo Log。
- Read View。
Undo Log
Undo Log 保存数据修改前的旧版本。
例如把 name 从 刘备 改成 曹操,undo log 会记录修改前的值。这样事务回滚时可以恢复旧值,快照读时也可以沿着版本链找到对当前事务可见的版本。
Undo Log 的作用:
- 支持事务回滚。
- 支持 MVCC 版本链。
一个事务提交后,undo log 不一定立刻删除,因为可能还有其他事务的 Read View 需要读取旧版本。
Read View
Read View 可以理解为事务生成的一张可见性快照。
它会记录生成快照时系统中活跃的事务 ID,并根据规则判断某个数据版本对当前事务是否可见。
判断时通常关注:
- 当前活跃事务 ID 列表。
- 最小活跃事务 ID。
- 下一个即将分配的事务 ID。
- 当前事务自身 ID。
MVCC 的核心问题就是:版本链中哪个版本对当前事务可见。
RC 和 RR 下 Read View 的差异
Read Committed 和 Repeatable Read 都可以使用 MVCC,但生成 Read View 的时机不同。
| 隔离级别 | Read View 生成时机 |
|---|---|
| RC | 每次普通 SELECT 都生成新的 Read View |
| RR | 第一次普通 SELECT 生成 Read View,事务内复用 |
所以 RC 下同一事务两次查询可能看到其他事务新提交的数据;RR 下同一事务多次查询结果更稳定。
当前读和快照读
InnoDB 读操作可以分为当前读和快照读。
快照读读取的是符合 Read View 的历史版本,普通 SELECT 通常是快照读。
当前读读取最新版本,并且可能加锁,例如:
select * from user where id = 1 for update;
update user set name = '曹操' where id = 1;
delete from user where id = 1;
当前读要读取最新数据,因为它可能要修改记录,不能只看旧版本。
Insert、Update 和 Delete 的事务视角
CUD 操作通常属于当前读逻辑。
以 Update 为例,执行时需要找到当前最新记录并加锁,修改数据后生成新的版本,同时写 undo log 和 redo log。
这也是为什么写操作之间会发生锁等待,而普通快照读可以不阻塞写。
小结
MySQL 事务可以这样串起来:
- ACID 描述事务目标。
- 隔离级别是在正确性和性能之间做权衡。
- 锁能解决并发冲突,但会降低并发能力。
- MVCC 用多版本让普通读不加锁。
- Undo Log 保存历史版本。
- Read View 判断哪个版本可见。
- 当前读看最新数据,快照读看事务视图中的数据。
理解 MVCC 后,再看 InnoDB 锁机制和 RR 下的幻读处理,就会清楚很多。