PostgreSQL 在读已提交级别执行普通表读取时,会使用为当前语句建立的快照。即使另一个事务在读取尚未结束时提交,表扫描仍沿用原来的可见性边界;下一条语句则可能看到新提交。因此,同一个事务可以得到各自一致、却在两条语句之间不同的答案,并不表示任何一条语句看到了别人未提交的更新中间状态。先明确观察边界,才能判断这种变化是不是业务允许的结果。

指明真正使用的边界

读已提交是 PostgreSQL 的默认隔离级别,但角色、数据库或会话可以修改配置默认值。因此,只看到不带限定的 BEGIN,不能证明应用到底用了哪一档。下面的实验明确选择读已提交。可重复读则复用事务第一条非事务控制语句建立的快照;仅仅开始事务,并不等于已经取得那份实际读取快照。

快照约束的是其他事务修改的可见性。读取事务自己此前执行的写入,仍然对后面的语句可见。因此,一致快照并不是把所有来源的值都无条件冻结,也不保证应用稍后根据旧结果行动时,相关数据仍然是最新的。读取时看到一致状态,与写入提交时仍满足决策前提,是不同要求。

坏掉的样子

两次读已提交查询可以看到不同的已提交数据集合。例如先统计数量、等待一段时间,再取对应记录,如果其他事务在中间修改数据,数量与列表可能对不上。若业务要求这两项观察来自同一视图,应组合为合适的一条查询,或者为跨多条语句的读取选择适当的事务级快照。不能用“它们放在同一个事务里”替代隔离级别的具体保证。

也不要把普通读取规则推广到所有写入或加锁读取。读已提交下,更新可能先等待另一个写者,随后针对更新后的行重新判断条件;SELECT FOR UPDATE 也有明确的加锁行为。此前稳定的读取,并没有预订那一行,更没有自动建立跨行业务不变量。隔离级别、原子更新和显式协调分别回答不同的并发问题,需要根据实际操作组合使用。

普通表扫描的边界,也不能直接推广到 VOLATILE SQL 或过程语言函数内部发起的查询。这类函数可以为内部查询取得新快照,STABLE 和 IMMUTABLE 函数则使用调用查询的快照。本例的建议锁门闩没有再次读取业务表,因此不能据此推断,任意通过函数组装的报表都共享同一条可见性边界。

快速验证

在可丢弃的 PostgreSQL 18 数据库中打开甲、乙两个新的 psql -X -v ON_ERROR_STOP=1 会话,使用同一角色,显式事务之外保留正常自动提交。模式名称和建议锁键仅供本实验使用。建议锁只是控制执行先后的门闩,并不锁住正在被修改的业务表记录。

  1. 在乙中创建初始值,并持有会话级门闩。
CREATE SCHEMA statement_snapshot_demo;
CREATE TABLE statement_snapshot_demo.item (id integer PRIMARY KEY,value integer);
INSERT INTO statement_snapshot_demo.item VALUES (1,100);
SELECT pg_advisory_lock(8610,1);
  1. 在甲中启动读已提交语句。它建立语句快照后会等待门闩;保持它运行,再操作乙。
SET application_name='xbcatm_snapshot_reader';
SET statement_timeout='60s';
BEGIN ISOLATION LEVEL READ COMMITTED;
WITH gate AS MATERIALIZED (SELECT pg_advisory_xact_lock(8610,1))
SELECT value FROM statement_snapshot_demo.item CROSS JOIN gate WHERE id=1;
  1. 在乙中观察甲。只有结果显示 Lock 与 advisory 后才继续,证明语句确实已经开始执行,而不是根据估计的等待时间猜测。
SELECT wait_event_type,wait_event FROM pg_stat_activity
WHERE application_name='xbcatm_snapshot_reader' AND state='active';
  1. 仍在乙中通过自动提交写入新值,然后释放门闩,再返回甲收集读取结果。
UPDATE statement_snapshot_demo.item SET value=200 WHERE id=1;
SELECT pg_advisory_unlock(8610,1);
  1. 在甲中发起新的读取并结束事务,最后在乙中移除夹具。
SELECT value FROM statement_snapshot_demo.item WHERE id=1;
COMMIT;
DROP SCHEMA statement_snapshot_demo CASCADE;

原文的 PostgreSQL 18.6 观察未在本轮重跑,其中甲确实停在建议锁等待上。乙在释放门闩之前已经把存储值改为二百,解锁函数返回真;但甲那条已经开始运行的语句仍返回一百。甲在同一个显式事务里执行的下一条语句,才返回二百。这个门闩让执行顺序可以观察,不依赖操作人员复制命令的速度,也不把单纯等待几秒当作并发证据。

甲取得的事务级建议锁会一直保留到提交,所以应先让甲结束,再清理对象。六十秒的语句超时用于限制忘记释放门闩的等待。如果先发生超时,就回滚甲、释放乙的门闩,并清理夹具后再重新开始;被取消的语句没有给出成功的快照观察,不能沿用正常完成情况下的结果解释。

把结果用于正确的决策

实验展示的是这次表扫描的语句快照边界,并没有授权应用无条件复用旧决策。可重复读可以让先前结果保持稳定,但在同一快照中重读,并不能发现后来已提交的变化。更强隔离级别下的写事务也可能收到序列化失败,重试时需要重新计算完整操作,而不是只补发最后一条写语句。

如果是在生成报表,应明确一条一致查询,还是多条共享快照的查询,才符合输出要求。如果是在执行状态转换,应明确写入提交时必须仍然成立的条件,再选择相应并发协议。一致读取与并发决策正确确实相关,但不能把前者当成后者已经得到保证的证据。

来源