03 · 事务与隔离级别:ACID / 幻读 / MVCC
后端"并发扣款""下单库存"这类问题的答案在事务与隔离级别里。本篇讲清 ACID 每一条在防什么、四个隔离级别各自漏什么(脏读/不可重复读/幻读),以及 MVCC 怎么在不锁读的前提下保证一致性。
速览卡
| 项 | 内容 |
|---|---|
| 一句话定位 | 数据库并发一致性的底层机制:ACID → 隔离级别 → MVCC |
| 核心内容 | ACID 四性、四个隔离级别与三种异常、MVCC 快照读、当前读与锁 |
| 读完能回答 | READ COMMITTED 和 REPEATABLE READ 差在哪?幻读到底指什么?MySQL 默认级别为什么能防幻读? |
| 相关板块 | DB01 关系模型 | DB02 索引 | C04 一致性与事务边界 |
本篇讲数据库内的事务语义;跨服务、跨库的分布式事务边界见 C04。
一、ACID:每一条在防什么
| 特性 | 含义 | 防止什么 |
|---|---|---|
| 原子性 A | 全做或全不做 | 一半成功一半失败 |
| 一致性 C | 事务前后约束都成立 | 违反业务/约束规则 |
| 隔离性 I | 并发事务互不干扰 | 读到别人的半成品 |
| 持久性 D | 提交后不丢 | 宕机/断电丢数据 |
常见误解:C 不是数据库自动给的。约束(外键、CHECK、唯一)能帮一部分,但业务一致性要应用自己保证——这也是为什么"扣库存要在一个事务里校验+扣减"。
二、三种并发异常
| 异常 | 现象 | 什么时候发生 |
|---|---|---|
| 脏读 | 读到未提交的数据(别人回滚了你就读到假数据) | 隔离级别 < READ COMMITTED |
| 不可重复读 | 同一事务内两次读同一行,值变了(被更新/删除) | 隔离级别 < REPEATABLE READ |
| 幻读 | 同一事务内两次范围查询,行数变了(被插入) | 隔离级别 < SERIALIZABLE(且无间隙锁) |
记忆:脏读=读到别人的未提交;不可重复读=同一行值变了;幻读=范围里多出/少出行。
三、四个隔离级别:一个递进关系
| 级别 | 脏读 | 不可重复读 | 幻读 | 代价 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 最低 |
| READ COMMITTED(PG 默认) | ❌ 防 | ✅ 可能 | ✅ 可能 | 低 |
| REPEATABLE READ(MySQL 默认) | ❌ 防 | ❌ 防 | ⚠️ 见下 | 中 |
| SERIALIZABLE | ❌ 防 | ❌ 防 | ❌ 防 | 最高(并发最差) |
MySQL 与 PG 默认不同:MySQL 默认
REPEATABLE READ,Postgres 默认READ COMMITTED。 别拿"MySQL 的默认"套所有库。
四、MVCC:不加锁的快照读
**MVCC(多版本并发控制)**让"读"不阻塞"写":
写:生成新版本(带上事务号)
读:按自己事务的快照,只看到"提交时已可见"的版本4.1 快照读 vs 当前读
| 快照读 | 当前读 | |
|---|---|---|
| 形式 | 普通 SELECT | SELECT ... FOR UPDATE、UPDATE、DELETE |
| 行为 | 读历史快照,不加锁 | 读最新已提交,要加锁 |
| 并发性 | 高 | 低 |
关键认知:普通 SELECT(快照读)在读旧数据,所以高隔离级别下"读到的不是最新值"是设计如此。 需要"读到最新再判断"时必须用当前读(
FOR UPDATE)。
4.2 MySQL 的 REPEATABLE READ 怎么防幻读
普通快照读天然看不到新插入的行(读的是旧快照)。 但 SELECT ... FOR UPDATE 这类当前读要防幻读,靠的是**间隙锁(Gap Lock)**把范围锁住,不让别人往范围内插入。
结论:MySQL 默认级别下,快照读不产生幻读,当前读靠间隙锁防幻读。这不是"幻读消失了",而是"被机制挡住了"。
五、后端最常踩的三类事务坑
| 坑 | 现象 | 对策 |
|---|---|---|
| 长事务 | 锁持太久、undo log 膨胀 | 事务只包必要的写,别把 HTTP 调用/IO 放事务里 |
| 事务里做远程调用 | 数据库锁着等外部响应 | 事务外先取数据,事务内只做本地写 |
| 读后写不加锁 | 并发扣库存变负 | 用 FOR UPDATE 或 UPDATE ... WHERE stock > 0 条件更新 |
-- 条件更新:把"先查后改"变成"一条原子语句"
UPDATE inventory
SET stock = stock - 1
WHERE id = 1 AND stock > 0; -- 受影响行数为 0 即库存不足事务边界的设计属于后端一致性,完整讨论见 C04。
六、自检清单
- [ ] 能说出 ACID 四条各自防什么
- [ ] 区分脏读/不可重复读/幻读三种异常
- [ ] 知道 MySQL 与 PG 的默认隔离级别不同
- [ ] 知道 MVCC 让普通 SELECT(快照读)不加锁
- [ ] 需要"读到最新再判断"时用当前读(FOR UPDATE)
- [ ] 知道长事务与事务内远程调用的危害
- [ ] 会写条件更新(
WHERE stock > 0)做并发防护
七、常见误区
| 误区 | 事实 |
|---|---|
| "隔离级别越高越好" | SERIALIZABLE 并发最低,够用就行 |
| "MySQL 默认 = PG 默认" | 分别是 REPEATABLE READ 与 READ COMMITTED |
| "REPEATABLE READ 就不会读旧值" | 快照读就是读旧版本,这是特性不是 bug |
| "事务里啥都放" | 远程调用/慢 IO 进事务会拖长锁 |
| "先 SELECT 再 UPDATE 就安全" | 并发下两个事务都能通过检查,要用当前读或条件更新 |
| "数据库保证一致性" | 数据库保证约束,业务一致性要应用设计 |
八、动手练习
练习 A:复现不可重复读
开两个事务:事务 A 连读同一行两次,事务 B 在中间更新该行并提交。对比 READ COMMITTED 与 REPEATABLE READ 下 A 的两次读值。
练习 B:设计扣库存
要求:库存不能为负、高并发。写一条 SQL(条件更新),并说明它为什么不需要"先查再锁"。
练习 C:给一个导出报表加隔离级别
报表要"一致的快照"但允许延迟,你会选哪个级别?为什么不用 SERIALIZABLE?
相关文档
本区
- 01 · 关系模型与 SQL 基础 —— 集合思维先行
- 02 · 索引原理与优化 —— 当前读与锁依赖索引
- 10 · 数据迁移与双写 —— 迁移时的一致性校验
其他域
- 04 · 一致性与事务边界 —— 分布式事务与幂等
- 10 · 数据建模与数据访问 —— ORM 里事务的用法