当变更一直等待
一句话总结
锁等待、SQL 执行、重试次数是彼此不同的预算。不能仅凭收到了超时,就判断业务变更没有发生,或者再次执行是安全的。
为什么需要它
想撤销两笔庆典订单的取消,画面却一直在转圈。第一笔订单已经修改了,第二笔订单被其他负责人占着。这时如果不管不顾地发送新请求,等待执行的请求就会增加。快速按按钮并不能解决问题,反倒是让等待同一个资源的队伍变得更长。要向客户说明什么时候可以再按,就必须知道在哪里等了多久、已经确认了什么。
前面的实验把批准的版本放进 UPDATE 条件里,不去覆盖其他变更。但条件安全和响应迅速,是不同的性质。就连不匹配的条件,也要等锁释放后才能评估。要设定上限,避免无限期等待;失败之后,还要确认同一个连接是否处于可以交给下一个请求的状态。修改过设置的连接回到连接池之后,把其他用户的请求也在很短的时间内切断,同样是事故。
工作原理
PostgreSQL 的 lock_timeout 限制单次获取锁的尝试所等待的时间。statement_timeout 限制服务器处理语句的时间。一个批次中如果有多条 UPDATE,每条语句都有各自的获取锁,所以这两个设置没有一个等同于整个批次的耗时上限。整个 API 的时间预算,还包括连接、多条 SQL、重试之间的等待以及响应传输。
本实验先把小批次的数据库等待和执行分开来观测。锁上限设为 50–500ms,语句上限设为比它大、且不超过 2000ms。如果语句上限比锁上限短,整条 SQL 会先被取消,就很难区分锁专用的上限起了什么作用。这些数字是学习环境的契约,而不是建议在生产环境原样使用的推荐值。
设置要在事务内通过把 set_config 的第三个参数设为 true 来应用。函数成功,或因错误而回滚之后,借来的连接必须保留原来的设置。如果使用会话范围的设置,即使事务成功之后,上限也可能留下来。只有“失败时已回滚”这一个测试,会漏掉这种泄漏。所以成功和失败两条路径,都要检查设置值以及是否有打开的事务。
依次撤销两行的过程中,如果在第二行出现超时,第一行也必须回滚。让已经执行过的第一条 UPDATE 回退的,不是 Python 的异常名称,而是外层数据库事务。要把异常传播出去,让出错的上下文正常退出,并确认连接处于 IDLE 状态。如果带着失败的事务不关闭,从下一条 SQL 开始重试,只会让错误叠加。
在现场相遇的样子
这次只对获取锁失败的 SQLSTATE 55P03 做有限的重试。语句取消 57014、批准内容冲突、输入错误、原因不明的连接错误,都传递给调用者。不要用中文或英文字符串去比较错误消息,而要使用驱动的错误分类。仅凭分类,并不能保证所有情形下重试都是安全的。这个示例同时使用了补偿请求 ID 和批准内容固定、失败尝试的事务会回滚这一契约。
retry_undo 的 attempts 是包含首次调用在内的总尝试次数。为 3 时,是首次一次加重试两次。等待函数接收失败的尝试编号 1、2,最后一次失败之后不再调用。在服务中,可以往这个等待函数里加入有上限的延迟和抖动,但本课只实现次数限制。不要把它称作实现了整体耗时截止,或所有服务器并发请求数的限制。
锁释放之后,也不能保证批准的值仍然相同。如果让你等待的那位负责人修改了值并提交,下一次尝试就必须以 Conflict 停下。为了让重试成功而把期望版本换成当前值,原来的批准保护就消失了。这就是因为等待而失败的请求,与因为批准内容过期而被拒绝的请求,接下来的行动必须不同的原因。
下一项检查要做什么
在测验中区分锁、语句、尝试预算和设置泄漏。在下一个模块的综合实验中,会让另一个连接占住第二行来观测真实的 55P03,并用延迟触发器引发 57014。用 SQL 来确认:同一个连接是否已被清理、第一行是否没有留下、锁释放之后同一个补偿 ID 的重试是否只产生了一次效果。
参考:PostgreSQL 客户端连接默认设置、错误码、psycopg 事务管理。于 2026-09-13 确认,实际运行环境是 PostgreSQL 16 和 psycopg 3.2.3。