跳转到内容

Read Committed 和 Repeatable Read 到底差在哪

先记住阶段三的结论:旧版本不会被就地覆盖。隔离级别的全部差异,归根到底只是「在什么时刻取快照」的差异。

Postgres 的默认隔离级别。关键规则:每条语句开始执行时,各自取一次新快照。

Read Committed编排回放 · 非实时执行
Session A
Session B
0 / 8

A 从头到尾没做任何写操作,却在一个事务内看到了两个不同的世界。对于「先查余额,再据此扣款」这类逻辑,这是实打实的 bug 温床。

改一个字:BEGIN ISOLATION LEVEL REPEATABLE READ。规则变成:整个事务共用第一条语句取的那一个快照。

Repeatable Read编排回放 · 非实时执行
Session A
Session B
0 / 8

快照隔离不是免费的。如果 A 在 RR 事务里也想写同一行:

Repeatable Read 下的写冲突编排回放 · 非实时执行
Session A
Session B
0 / 6

用 Repeatable Read 就必须写重试逻辑。这是很多人从 MySQL 迁过来踩的第一个坑:MySQL 的 RR 在这种情况下会用「当前读」直接改掉新值,而 Postgres 选择报错。哪种更好可以争论,但 Postgres 的行为至少是显式的——它不会让你在不知情的情况下基于过期数据完成写入。

单会话也能观察到「快照在何时确定」。下面两段的差别只有隔离级别:

准备中…
BEGIN;
SELECT pg_snapshot_xmin(pg_current_snapshot()) AS snapshot_xmin;
⌘/Ctrl + Enter
SELECT pg_snapshot_xmin(pg_current_snapshot()) AS snapshot_xmin_second_call;
COMMIT;
⌘/Ctrl + Enter

在 Read Committed 下,两次调用拿到的快照边界会随着其它事务的推进而变化;把第一段改成 BEGIN ISOLATION LEVEL REPEATABLE READ; 再跑一遍,快照就被钉住了。

先自己回答,再点开对照。

Read Committed 和 Repeatable Read 的差别,用「取快照的时机」一句话概括。

Read Committed 每条语句开始执行时各取一次新快照;Repeatable Read 整个事务共用第一条语句取的那一个快照。

差别只有这一条,其余现象都是它的推论:RC 下 B 提交之后 A 的下一条 SELECT 会取到新快照,于是同一条 SQL 读出不同的值(不可重复读);RR 下 A 的快照被钉住,B 提交与否都影响不到它,旧版本还躺在页面上,照读不误。

常见错误:以为 RR 的快照在 BEGIN 时就确定了。BEGIN 只是开启事务,快照是在第一条真正访问数据的语句执行时才取的。所以 BEGIN 之后、第一条 SELECT 之前这段时间里别人提交的改动,RR 事务是能看到的。想在某个精确时刻钉住快照,就要让第一条语句紧跟 BEGIN

详见快照与可见性判断

为什么 Postgres 的 Repeatable Read 不会发生幻读?

因为 Postgres 的 RR 实现是快照隔离(Snapshot Isolation):事务看到的是数据库在某一时刻的整库一致切片,而不只是「已经读过的那几行不变」。

既然是整库切片,别人新插入的行自然也在快照之外——SELECT count(*) FROM orders 在事务里反复执行,结果恒定。防止不可重复读和防止幻读在这个实现下是同一件事,不需要额外机制。

常见错误:把 SQL 标准里「RR 允许幻读」这条背下来的正确知识直接迁到 Postgres,于是为了防幻读把隔离级别升到 SERIALIZABLE在 Postgres 上这一步换不到任何东西——幻读在 RR 就已经没有了,白白付出 SSI 的跟踪开销和序列化失败回滚。SERIALIZABLE 真正多解决的是另一类问题(写偏斜),和幻读无关。

详见可串行化:Postgres 的 SSI 是怎么回事

为什么 READ UNCOMMITTED 在 Postgres 里没有意义?

因为 MVCC 架构下根本读不到未提交的版本。可见性判断的依据是元组头的 xmin / xmax 加上那个事务是否已提交——一个未提交事务写下的新版本,对任何人都不可见,没有一条代码路径能绕过这个判断。

所以 Postgres 接受 READ UNCOMMITTED 这个语法,但行为等同于 Read Committed。这也是为什么实际可选的隔离级别只有三个。

常见错误:以为设成 READ UNCOMMITTED 就能读到别人事务中间态的数据,用来「看看那个大批量导入跑到哪了」。这条语句不报错、静默生效为 Read Committed——正是因为不报错,很容易以为已经生效了,然后对着一个和默认级别完全一样的结果得出错误结论。上面 Read Committed 的时间线里 B 未提交那一步就是证据:A 读到的仍是 32.49。

用 Repeatable Read 时,应用层必须额外准备什么?

重试逻辑。 RR 事务去写一行「在自己的快照之后被别人改过」的数据时,Postgres 会直接报 ERROR: could not serialize access due to concurrent update 并让事务失败,由应用决定怎么办——重跑整个事务,重新取一次快照。

常见错误:从 MySQL 迁过来,以为 RR 下 UPDATE 被行锁挡住、等前一个事务提交后解除阻塞,自己就会在新值上继续执行。MySQL 的 RR 在写路径上用的是「当前读」,会绕过快照直接改最新值;Postgres 选择报错。 这个差异最恶劣的地方在于它只在真并发下暴露——开发和测试环境永远撞不上,代码看起来完全正常,上线后随着并发上升开始零星报错。