事务可能失败,剩下的就靠你重试
PostgreSQL 中,另一个事务修改了同一行,不代表当前事务必然失败。结果取决于隔离级别、语句怎样使用数据,以及竞争者最终提交了什么。把所有碰撞都归成“等一下就好”,会掩盖无效输入,也可能重复使用已经过时的业务决策。需要定义的重试单位,是包含相关读取、判断与写入的一次完整操作。 坏掉的样子 已提交读下,普通查询使用本条语句的快照。更新遇到竞争写者时,可以先等待,再针对对方提交后的行版本重新检查条件。因此,两个把字段加一的更新完全可能先后成功。另一个常见写法却不同:客户端先读取数字,在内存里算好绝对值,稍后再把那个常量写回。数据库不会自动替客户端重算这段业务逻辑,也不能把这两种写法混为一谈。 可重复读在事务第一条非事务控制语句处建立快照,随后通常沿用。如果目标行在这个快照之后被别人修改并提交,当前事务尝试更新时就可能立即收到序列化失败。可串行化还会识别跨不同行形成的依赖异常。它们都没有承诺“只有提交才报错”,隔离更强也不等于每个负载都会更早、更频繁地失败。执行语句与提交事务这两个位置都必须检查结果。 诊断时应记录第一次错误的 SQLSTATE、隔离级别和操作标识。后续查询出现的失败事务状态只是前面错误的结果;如果日志只留下最后一条查询,就可能把真正导致中止的原因覆盖掉。 保存点可以让某些语句错误恢复后继续较早的工作,但不能代替序列化失败后从新事务快照开始的完整重试。继续使用原来的读取与判断,可能再次作出失效决策。自动重试循环中的外部副作用,也必须有自己的去重契约;数据库回滚不会自动撤回已经发出的邮件或其他系统已接受的操作。 快速验证 准备 PostgreSQL 18 的可丢弃数据库,用两个全新的 psql -X -v ON_ERROR_STOP=1 连接,分别记作甲和乙。严格按下面的先后顺序执行,未明确开启事务时,乙保持自动提交。甲先建立独立实验模式和计数器,不要把示例放进生产库: CREATE SCHEMA conflict_demo; CREATE TABLE conflict_demo.counter (id integer PRIMARY KEY,n integer); INSERT INTO conflict_demo.counter VALUES (1,0); 甲开启已提交读事务并读取零。读取完成后先不要提交,让乙获得插入调度的机会: BEGIN ISOLATION LEVEL READ COMMITTED; SELECT n FROM conflict_demo.counter WHERE id=1; 乙把数字加一并提交,返回一: ...