Skip to content

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(多版本并发控制)**让"读"不阻塞"写":

text
写:生成新版本(带上事务号)
读:按自己事务的快照,只看到"提交时已可见"的版本

4.1 快照读 vs 当前读 ​

快照读当前读
形式普通 SELECTSELECT ... 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 条件更新
sql
-- 条件更新:把"先查后改"变成"一条原子语句"
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?


相关文档 ​

本区 ​

其他域 ​