卷 IV · 事务CH 17深度 17/24

隔离级别:脏读、幻读与写偏斜

上一章说「隔离级别只是快照拍法的不同」。这一章把这句话兑现:同样的两个事务交错,在不同隔离级别下会得到不同的结局——有的会露出破绽(读到前后矛盾的数据),有的滴水不漏。你需要知道每一档挡住了什么、放过了什么。

隔离级别不可重复读幻读SERIALIZABLE

并发会露出哪些破绽

SQL 标准定义了几种「并发异常」——两个事务交错时可能出现的、违反直觉的现象。从轻到重:

  • 脏读(dirty read):读到了别人还没提交的改动。万一那个事务回滚了呢?你就读到了一个从未真正存在的值。
  • 不可重复读(non-repeatable read):同一事务里,两次读同一行,值不一样——因为中间别人改了并提交了。
  • 幻读(phantom read):同一事务里,两次同样的范围查询,行数不一样——因为中间别人插入 / 删除了满足条件的行。
  • 写偏斜(write skew):两个事务各自基于「旧世界」做决定、各自都合法,合在一起却违反了一个全局规则(经典例子:两个值班医生同时看到「还有另一人在岗」,于是各自请假,结果没人值班了)。

四档隔离,各挡住一部分

隔离级别就是「你愿意容忍哪些破绽、换取多少并发」的旋钮。SQL 标准定义四档,PostgreSQL 的实际行为如下:

隔离级别脏读不可重复读幻读写偏斜
READ UNCOMMITTEDPG 里也不会可能可能可能
READ COMMITTED(PG 默认)不会可能可能可能
REPEATABLE READ不会不会PG 里也不会可能
SERIALIZABLE不会不会不会不会

两处 PostgreSQL 的「超纲」值得注意:

  • PostgreSQL 根本没有脏读——就算你要求 READ UNCOMMITTED,它也当作 READ COMMITTED 执行。因为 MVCC 下,你读的永远是某个已提交的版本,天生读不到未提交的脏数据。
  • PostgreSQL 的 REPEATABLE READ 顺手挡住了幻读——这比 SQL 标准要求的更强。标准里 RR 只保证不可重复读不发生、幻读仍可能;但 PG 的 RR 是基于「整个事务一张快照」的快照隔离,一张凝固的快照里,别人新插入的行你根本看不见,幻读自然也没了。
▶ 动手 · 同样的交错,不同的结局

下面这台真引擎复现两种异常。选隔离级别(READ COMMITTED / REPEATABLE READ),选场景(不可重复读 / 幻读),看同样的交错怎么在 RC 下露出破绽、在 RR 下被挡住。

盯着「不可重复读」这个场景:在 READ COMMITTED 下,T2 第一次读到余额 100,中间 T1 改成 30 并提交,T2 再读就变成了 30——同一事务里两次读,值变了,这就是异常出现(红框)。切到 REPEATABLE READ,T2 两次都读到 100——它全程用开始时那张快照,看不见 T1 的提交,异常被挡住(绿框)。「幻读」场景同理,只是从「值变了」变成「行数变了」。

该选哪一档?

  • READ COMMITTED(默认):绝大多数 OLTP 应用够用。每条语句看最新已提交数据,并发好。但要清楚它不保证「同一事务里两次读一致」——如果你的逻辑依赖这个,得升级。
  • REPEATABLE READ:需要「整个事务看到一致快照」时用——报表、跨多条语句的一致性计算、「读—判断—写」必须基于同一份数据的逻辑。代价:如果你更新的行被别人抢先改了,PG 会报 could not serialize access 让你重试
  • SERIALIZABLE:需要「就像事务一个接一个串行跑」的最强保证时用——能挡住连 RR 都放过的写偏斜。PG 用一套叫 SSI(Serializable Snapshot Isolation)的机制,在运行时检测「危险的读写依赖环」,一旦发现就让其中一个事务失败
◆ 用 SERIALIZABLE / RR,就必须写重试

这是用高隔离级别的必备心法:在 REPEATABLE READSERIALIZABLE 下,事务可能因为并发冲突而被数据库主动中止,报出 serialization_failure(SQLSTATE 40001)。这不是 bug,是设计——它在说「你俩撞了,我不能同时让你们都对,你重来一次」。所以你的应用必须包一层重试循环:捕获这个错误,回滚,重新执行整个事务。有了重试,SERIALIZABLE 就能用乐观的方式(不加大量锁)给你最强的正确性保证——这是 PostgreSQL 并发设计的精妙之处。

⇄ 对照 MySQL
  • 默认级别就不一样!PostgreSQL 默认 READ COMMITTED,MySQL(InnoDB)默认 REPEATABLE READ。从 MySQL 迁过来,如果你的代码依赖了「事务内两次读一致」这个 MySQL 默认给的保证,到了 PG 的 RC 下它就不成立了——这是个隐蔽而危险的迁移坑,务必检查。
  • RR 挡幻读的机制不同:MySQL 的 RR 靠间隙锁 / next-key 锁(悲观加锁)防幻读,代价是更容易死锁、更容易阻塞。PostgreSQL 的 RR 靠快照(乐观),不加范围锁,冲突时报错让你重试。两条不同的哲学。
  • SERIALIZABLE:MySQL 的 SERIALIZABLE 基本靠给所有读加锁实现(并发差);PostgreSQL 的 SSI 是无锁的依赖检测,并发好得多,但要求你写重试。

这一章的一句话

隔离级别是「容忍哪些破绽」的旋钮:PG 默认 READ COMMITTED(无脏读,但有不可重复读 / 幻读),REPEATABLE READ 用一张快照挡住二者(比标准更强),SERIALIZABLE 用 SSI 连写偏斜都挡——但后两档要求你的应用会重试。

MVCC 让并发如此顺滑,代价是死元组。下一章,我们直面这个账单:死元组怎么积累、怎么让表膨胀,以及 VACUUM 怎么替你打扫战场。