ShedLock 获取锁后,jOOQ 业务事务竟提前提交
ShedLock 7.9.0 与绑定原始连接的 jOOQ 组合可能提前提交业务写入。采用独立连接的锁事务,区分 Spring 回调与回滚边界。
批处理还没调用 commit(),另一条连接却已经能读到刚写的数据;随后执行 rollback(),数据也没有撤回。如果中间调用过 ShedLock 的 JooqLockProvider,先确认锁和业务是否共用了同一个原始 JDBC Connection。
这条故障可以没有异常。明确受影响的组合是 ShedLock 7.9.0,DSL.using(connection, dialect) 绑定调用方手工管理、已经关闭自动提交的连接。锁操作成功,甚至抢锁失败,都可能触发那条连接上的提前提交。
业务连接被交给了另一套事务边界
单参数 JooqLockProvider 把存储操作交给 jOOQ 的 transactionResult()。jOOQ 管理的是自己配置中的事务层级;调用方先在原始连接上开启的事务,不会自动变成它已知的一层嵌套事务。锁操作结束时,提交会落到这条物理连接上,把此前尚未提交的业务写入一起提交。
检查创建 DSLContext 的入口,而不只是 @SchedulerLock 注解。DSL.using(rawConnection, ...) 与从连接池取得连接的配置不同;外面再包一层 Spring REQUIRES_NEW,也不能把已经固定在 DSLContext 里的原始连接换走。
不要用 setAutoCommit(true) 规避。对已有活动事务的 JDBC 连接切回自动提交,本身就可能提交那段事务,会把同一个问题移到应用代码里。
让锁事务取得独立连接
ShedLock 7.10.0 增加了 DataSource 支持的 JooqTransactionRunner,7.10.1 的正式实现保留该入口。保留原业务 DSLContext,给锁提供能取得独立连接的真实连接池:
import javax.sql.DataSource;
import net.javacrumbs.shedlock.core.LockProvider;
import net.javacrumbs.shedlock.provider.jooq.JooqLockProvider;
import net.javacrumbs.shedlock.provider.jooq.JooqTransactionRunner;
import org.jooq.DSLContext;
static LockProvider lockProvider(DSLContext businessDsl, DataSource lockDataSource) {
return new JooqLockProvider(
businessDsl,
JooqTransactionRunner.usingDataSource(lockDataSource)
);
}
这是应用配置中的工厂片段。lockDataSource 应来自项目已有的、能提供独立物理连接的池,指向预期数据库并具备锁表权限;不要传入总是返回那条业务连接的单连接包装。连接池还需要有足够容量,业务事务持有连接时,锁事务仍能取到另一条连接。
这个 runner 从给定 DataSource 派生 jOOQ 配置后运行锁事务,避免在调用方的原始连接上结束事务。它不会自动修复已提交的数据,也不替代业务事务管理。
若项目依赖 Spring 事务同步回调,先核对原先是否使用 SpringNewTransactionRunner。DataSource runner 自己不经过 Spring 的 PlatformTransactionManager,不能假定原有事务事件监听仍会触发。已有的 Spring 事务感知连接配置,应按实际集成方式保留,而不是给所有 jOOQ 项目统一替换构造器。
验证回滚是否仍然有效
在隔离数据库中,用一条业务连接写入待回滚的标记,再从另一条观察连接检查可见性。分别覆盖成功获取锁、未获取锁、续锁和解锁:锁操作不应使业务标记提前对外可见;业务回滚后,标记仍应不存在,锁记录则按自己的生命周期变化。
这个检查需要真正独立的观察连接和适合的隔离级别,不能用原事务自己读到的内容判断是否已提交。还应覆盖业务提交成功、连接归还和池容量不足的失败路径。
已经被提前提交的业务写入,后续回滚无法撤销。先保留事务日志与受影响记录,按业务补偿流程处理;不要为了验证修复,在生产库重复执行那段写入。


