jOOQ SELECT FOR UPDATE NOWAIT未抛锁异常,陷入等待问题求助
问题根源分析
- 事务代理未生效:如果
runInSeparateTransaction是测试类内部方法,直接调用不会触发Spring事务代理,导致第二个查询仍在同一个事务中执行——同一事务内的行锁是共享的,自然不会触发锁冲突。 - 异常类型不匹配:jOOQ抛出的是
org.jooq.exception.DataAccessException,若测试中捕获的是Spring的org.springframework.dao.DataAccessException,会导致断言无法命中,甚至出现异常传递异常。 - 事务生命周期控制不当:测试方法被
@Transactional包裹时,第一个查询执行后事务未保持打开状态,锁提前释放,第二个查询不会触发异常。
解决方案步骤
1. 确保事务代理生效
将runInSeparateTransaction提取到独立的Spring Bean中,保证调用时通过代理触发新事务:
@Component public class TransactionTestHelper { @Transactional(propagation = Propagation.REQUIRES_NEW) public void runInSeparateTransaction(Runnable test) { test.run(); } }
在测试类中注入该Bean并调用,避免直接调用测试类内部方法。
2. 修正异常捕获类型
明确捕获jOOQ原生的DataAccessException:
import org.jooq.exception.DataAccessException; // ... testTransactionProvider.runInSeparateTransaction(() -> { assertThrows(DataAccessException.class, () -> { repository.findByIdForUpdateNoWait(id); }); });
若使用Spring的异常转换机制(通过JooqExceptionTranslator),也可捕获Spring的DataAccessException,但需确保转换逻辑生效。
3. 手动控制事务生命周期
避免测试方法被@Transactional包裹,手动控制第一个事务的打开与关闭,确保锁持续持有直到第二个查询执行完成:
@Autowired private PlatformTransactionManager transactionManager; @Test void findByIdForUpdateNoWait_fail() { UUID id = UUID.fromString("3d346f2f-9c44-4e8c-8479-f3eb4b46195f"); // 开启第一个事务并持有锁 TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition()); try { repository.findByIdForUpdateNoWait(id); // 在独立事务中执行第二个查询 testTransactionProvider.runInSeparateTransaction(() -> { assertThrows(DataAccessException.class, () -> { repository.findByIdForUpdateNoWait(id); }); }); } finally { transactionManager.rollback(status); // 最终释放锁 } }
4. 验证jOOQ生成的SQL正确性
确保jOOQ生成的SQL包含FROM子句(用户提供的日志可能存在遗漏),正确的SQL格式应为:
select "payment"."id" from "payment" where "payment"."id" = '3d346f2f-9c44-4e8c-8479-f3eb4b46195f' for update nowait
若SQL缺失FROM,需检查jOOQ代码生成器配置或实体类映射是否正确。
5. 版本兼容性检查
jOOQ 3.14.16与PostgreSQL 42.6.0理论上兼容,若问题仍存在,可尝试升级jOOQ到3.15+版本,修复可能存在的NO_WAIT语法支持bug。
验证方式
在repository方法中添加事务ID打印,确认两个查询处于不同事务:
public void findByIdForUpdateNoWait(UUID id) { Long txId = dslContext.select(field("txid_current()", Long.class)).fetchOne().value1(); System.out.println("当前事务ID: " + txId); dslContext.select() .from(PAYMENT) .where(PAYMENT.ID.eq(id)) .forUpdate() .noWait() .fetch(); }
若两次查询的事务ID不同,说明事务隔离生效,此时应正常抛出锁异常。
内容的提问来源于stack exchange,提问作者Markus Ridziauskas
相关产品推荐
相关产品推荐

