jOOQ 查询副本加了 NOWAIT,原查询也跟着变了
通过QOM复制带行锁的查询后,再修改副本的NOWAIT或锁表范围,可能同时改到原查询。问题在共享的ForLock对象,优先独立构建查询。
原查询只设置 FOR UPDATE,通过 QOM 方法得到副本,再给副本设置 NOWAIT,结果原查询的 SQL 也出现了 NOWAIT。这种变化发生在执行之前,不能先归因于 PostgreSQL 的锁等待策略。
在受影响路径中,查询外层对象虽然不同,里面的锁配置对象仍可能是同一个。
复制查询不一定复制锁对象
下面的片段展示这一条件,q 已经设置了行锁:
SelectQuery<?> copy = (SelectQuery<?>) q.$where(DSL.trueCondition());
copy.setForUpdateNoWait();
正常意图是只改变 copy。但正式3.21.9的 SelectQueryImpl.copyBetween() 直接赋值 result.forLock = forLock,没有为副本复制独立的 ForLock。后续 setForUpdateNoWait() 取得已有对象并修改等待模式,就可能同时影响 q。
范围也不只 NOWAIT。等待时间、SKIP LOCKED、锁定字段或表等可变锁属性,都需要核对是否走到共享对象。反方向修改原查询,也可能改变副本。
先在应用已有的 SQL 渲染检查中比较两份查询:保存修改前的原查询 SQL,修改副本后再渲染原查询。若原查询的锁子句已经变化,就已有客户端构造层的证据,无须到生产库里主动制造锁冲突。
分别构建带锁查询
暂时规避时,为两条操作分别调用同一个查询构建方法,让每次调用生成新的查询对象和锁配置。可以复用不变的表、字段和业务条件定义,但不要把已带锁的可变查询复制后,再把副本当成完全独立的对象修改。
构建方法必须保留各自真正需要的筛选条件、投影、事务上下文和锁范围。不要为消除串改直接删掉 FOR UPDATE:若业务靠行锁保护“读取后更新”,去掉锁可能引入并发覆盖。也不能给全部查询一律加 NOWAIT,它会改变等待和错误处理行为。
不要用同一查询对象在多个线程间反复切换锁模式。即使加了外层引用或重新声明局部变量,内部共享关系仍未改变。
检查锁行为和错误处理
先核对分别构建后的两条 SQL,确认改动一条时另一条保持原值。随后在获准的测试环境中,以两条独立连接验证原有等待行为、NOWAIT 的失败路径、事务结束后的释放,以及应用的重试规则。
客户端构造串改修复后,真实锁竞争仍然可能存在。如果报错只在 SQL 执行阶段发生,且两份 SQL 始终正确,应转向检查事务持有时间与锁冲突。
该缺陷已有后续版本修复目标,但截至2026年10月25日可核对的正式tag仍到3.21.9。版本号出现在修复计划里,不等于部署的依赖已经含有深拷贝处理;升级前仍需核对正式发布物。


