普通读取保留自己的语句快照
PostgreSQL 在读已提交级别执行普通表读取时,会使用为当前语句建立的快照。即使另一个事务在读取尚未结束时提交,表扫描仍沿用原来的可见性边界;下一条语句则可能看到新提交。因此,同一个事务可以得到各自一致、却在两条语句之间不同的答案,并不表示任何一条语句看到了别人未提交的更新中间状态。先明确观察边界,才能判断这种变化是不是业务允许的结果。 指明真正使用的边界 读已提交是 PostgreSQL 的默认隔离级别,但角色、数据库或会话可以修改配置默认值。因此,只看到不带限定的 BEGIN,不能证明应用到底用了哪一档。下面的实验明确选择读已提交。可重复读则复用事务第一条非事务控制语句建立的快照;仅仅开始事务,并不等于已经取得那份实际读取快照。 快照约束的是其他事务修改的可见性。读取事务自己此前执行的写入,仍然对后面的语句可见。因此,一致快照并不是把所有来源的值都无条件冻结,也不保证应用稍后根据旧结果行动时,相关数据仍然是最新的。读取时看到一致状态,与写入提交时仍满足决策前提,是不同要求。 坏掉的样子 两次读已提交查询可以看到不同的已提交数据集合。例如先统计数量、等待一段时间,再取对应记录,如果其他事务在中间修改数据,数量与列表可能对不上。若业务要求这两项观察来自同一视图,应组合为合适的一条查询,或者为跨多条语句的读取选择适当的事务级快照。不能用“它们放在同一个事务里”替代隔离级别的具体保证。 也不要把普通读取规则推广到所有写入或加锁读取。读已提交下,更新可能先等待另一个写者,随后针对更新后的行重新判断条件;SELECT FOR UPDATE 也有明确的加锁行为。此前稳定的读取,并没有预订那一行,更没有自动建立跨行业务不变量。隔离级别、原子更新和显式协调分别回答不同的并发问题,需要根据实际操作组合使用。 快速验证 在可丢弃的 PostgreSQL 18 数据库中打开甲、乙两个新的 psql -X -v ON_ERROR_STOP=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); 在甲中启动读已提交语句。它建立语句快照后会等待门闩;保持它运行,再操作乙。 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; 在乙中观察甲。只有结果显示 Lock 与 advisory 后才继续,证明语句确实已经开始执行,而不是根据估计的等待时间猜测。 SELECT wait_event_type,wait_event FROM pg_stat_activity WHERE application_name='xbcatm_snapshot_reader' AND state='active'; 仍在乙中通过自动提交写入新值,然后释放门闩,再返回甲收集读取结果。 UPDATE statement_snapshot_demo.item SET value=200 WHERE id=1; SELECT pg_advisory_unlock(8610,1); 在甲中发起新的读取并结束事务,最后在乙中移除夹具。 SELECT value FROM statement_snapshot_demo.item WHERE id=1; COMMIT; DROP SCHEMA statement_snapshot_demo CASCADE; PostgreSQL 18.6 实测中,甲确实停在建议锁等待上。乙在释放门闩之前已经把存储值改为二百,解锁函数返回真;但甲那条已经开始运行的语句仍返回一百。甲在同一个显式事务里执行的下一条语句,才返回二百。这个门闩让执行顺序可以观察,不依赖操作人员复制命令的速度,也不把单纯等待几秒当作并发证据。 ...