PostgreSQL 通过使用谓词锁定和序列图测试实现 可序列化快照隔离 (SSI),以在没有传统两阶段锁定性能惩罚的情况下实现真正的可序列化。40001 错误(序列化失败)特别发生在 写偏斜 或 读写冲突 中,其中两个事务建立了 rw-依赖 循环。例如,事务 A 读取满足谓词的行(例如,WHERE color = 'red'),事务 B 读取满足不重叠谓词的行(例如,WHERE color = 'blue'),然后 A 将行更新为 'blue' 而 B 将行更新为 'red'。两个事务都不阻塞对方,但结果是非可序列化的。
这种模式代表序列图中的一种危险结构:两个连续的 rw-反依赖 形成潜在循环。PostgreSQL 检测到这一点并中止一个事务以防止异常状态。这个问题是微妙的,因为这些事务可能修改不同的物理行,使得冲突对较低隔离级别使用的行锁机制是不可见的。
强制性解决方案要求应用程序实施 乐观重试循环。当捕获到 SQL EXCEPTION '40001' 时,应用程序必须回滚当前事务并重试整个操作,同时使用指数退后。与死锁不同,后者通常通过立即重试来解决,序列化失败在高争用下更受益于抖动延迟以防止打雷效应。
-- PL/pgSQL 中的应用重试逻辑示例 DO $$ DECLARE retries INT := 0; max_retries INT := 3; BEGIN WHILE retries < max_retries LOOP BEGIN SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE; PERFORM * FROM inventory WHERE category = 'electronics' AND count > 0; UPDATE inventory SET count = count - 1 WHERE item_id = 123; COMMIT; EXIT; EXCEPTION WHEN SQLSTATE '40001' THEN ROLLBACK; retries := retries + 1; PERFORM pg_sleep(power(2, retries) * 0.1); -- 指数退后 END; END LOOP; END $$;
一个演唱会票务交换平台允许用户通过检查-然后-执行逻辑交换座位类别。事务 A 验证 VIP 座位可用,然后将一个持有的 VIP 座位降级为标准座位。与此同时,事务 B 验证了标准座位的可用性并将一个标准座位升级为 VIP。在 READ COMMITTED 下,这两个事务都将可用性读为真,执行更新,系统最终在两个类别中都出现了负库存,尽管每个事务检查了约束。
构建了三种解决方案。第一种使用显式的 SELECT FOR UPDATE 锁定,但当可用性查询返回零行时失败,因为未获得任何锁定,使系统容易受到幻影插入的影响。第二种方法使用 pg_try_advisory_lock() 实现了 建议锁定 来序列化对座位类别的访问,避免了冲突,但引入了复杂的锁定顺序风险并导致吞吐量减少 40%,因为所有类别检查都被序列化。
第三种解决方案采用了 SERIALIZABLE 隔离级别并使用应用级重试循环。这是因为它保证了正确性而无需手动管理锁定,而且鉴于与读取操作相比,同时交换的频率较低,重试开销是可以接受的。实现使用了 JDBC 重试处理程序捕获 SQLException 和 SQLState 40001,等待 100ms * 2^attempt,并重新执行事务。这彻底消除了超额预订事件,尽管在高峰销售期,p99 延迟增加了 15ms。
可序列化隔离中的谓词锁与可重复读中的行锁之间的具体区别是什么?
可重复读 通过锁定实际返回查询的行来防止不可重复读取,但并不能防止幻影读取——其他事务插入的新行会满足查询的 WHERE 子句。可序列化 隔离使用 谓词锁 锁定搜索范围本身,防止任何匹配查询谓词的插入,即使在查询执行时该行不存在。候选人经常将这两者混淆,错误地认为 可重复读 防止幻影读取,或者认为 可序列化 仅锁定现有行。
当检测到循环时,序列图测试算法如何确定中止哪个事务?
PostgreSQL 使用“先提交者获胜”的策略以及危险结构检测。当并发事务之间形成 rw-冲突(读写依赖)时,系统跟踪此边缘是否在序列图中形成循环。完成循环的事务会以 SQLSTATE 40001 被中止。选择是基于图结构的确定性,而不是事务年龄,优先中止在检测到的循环中回滚成本最低或最新的事务。理解这一点是预防性中止(防止无效历史)而不是死锁(等待锁定)对于适当的错误处理至关重要。
为什么在序列化隔离检测到冲突的情况下,SELECT FOR UPDATE 可能会失败以防止序列化失败?
SELECT FOR UPDATE 仅对执行时存在的行获取 ROW SHARE 锁。在检查-然后-执行模式中,如果初始查询返回零行(例如,检查可用座位数量为零),FOR UPDATE 不会获取任何锁,从而允许另一个事务插入冲突行。可序列化 隔离将此视为谓词冲突,因为“零行”结果构成了一个有效的读取集,该读取集被并发插入无效化。候选人通常错误地认为 FOR UPDATE 提供了全面的保护,而没有意识到在谓词最初匹配任何内容时,它并不能对幻影插入提供防护。