You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JPA中版本实体结合悲观锁的无冲突实现(嵌套事务问题)

问题解决:嵌套事务下悲观锁与乐观锁冲突的处理

为啥会出这问题?

  1. 缓存坑了嵌套事务:外层事务启动后,如果之前加载过目标商户账户的PoolAccount实体,JPA的一级缓存会存着旧版本。内层调用悲观锁查询时,默认直接用缓存里的旧实例,不去数据库拿最新的加锁,导致版本号过时。
  2. 乐观锁认死理:因为实体带@Version注解,JPA存数据时一定会检查版本号。如果缓存里的版本号和数据库最新的不一样(比如其他事务改了),直接就抛出那个版本冲突的错误。
  3. 无外层事务就正常?:单独的事务会新建EntityManager,每次查询都直接从数据库捞最新实体加锁,没缓存干扰,自然不会错。

怎么解决?

方案1:让悲观锁查询强制跳过缓存

给悲观锁的查询方法加个注解,强制从数据库刷新最新实体,不管缓存里有没有:

public interface PoolAccountRepository extends JpaRepository<PoolAccount, Long> {
    // 其他方法不变

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints({@QueryHint(name = org.hibernate.annotations.QueryHints.HINT_REFRESH, value = "true")})
    @Query("SELECT da FROM PoolAccount da WHERE da.accountNumber = :accountNumber")
    PoolAccount getPoolAccountByAccountNumberForUpdate(@Param("accountNumber") String accountNumber);
}

为啥有用?:HINT_REFRESH=true会逼着JPA去数据库重新查最新的账户数据,加上行级写锁,同时拿到最新的version。后面存的时候,版本号和数据库一致,乐观锁就不会找茬了,而且悲观锁也能挡住其他事务改这行。

方案2:直接用SQL更新,绕开乐观锁

如果不想和JPA的乐观锁机制打交道,直接写SQL更新余额,跳过实体保存的版本校验:

先给Repository加个更新方法:

public interface PoolAccountRepository extends JpaRepository<PoolAccount, Long> {
    // 其他方法不变

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT da FROM PoolAccount da WHERE da.accountNumber = :accountNumber")
    PoolAccount getPoolAccountByAccountNumberForUpdate(@Param("accountNumber") String accountNumber);

    @Modifying
    @Query("UPDATE PoolAccount da SET da.balance = da.balance + :amount WHERE da.accountNumber = :accountNumber")
    void updateBalanceByAccountNumber(@Param("accountNumber") String accountNumber, @Param("amount") BigInteger amount);
}

然后改业务方法:

@Transactional(
        isolation = Isolation.READ_COMMITTED,
        rollbackFor = {Exception.class, RuntimeException.class},
        propagation = Propagation.REQUIRED)
void creditMerchantAccount(BigInteger amount){
    // 先锁行,不让别人改
    PoolAccount poolAccount = poolAccountRepository.getPoolAccountByAccountNumberForUpdate("123");
    BigInteger startingBalance = poolAccount.getBalance();
    BigInteger closingBalance = startingBalance.add(amount);
    
    // 直接更新余额,不用管乐观锁
    poolAccountRepository.updateBalanceByAccountNumber("123", amount);
    
    // 生成审计记录
    createTxnAudit(amount, startingBalance, closingBalance);
}

为啥有用?:JPQL更新语句不会触发乐观锁的版本检查,就单纯改余额。加上前面的悲观锁,确保这行在当前事务提交前别人动不了,既保证并发安全,又不触发版本冲突。

选哪个方案好?

  • 优先用方案1:不用改业务逻辑,加个注解就行,既保留JPA操作实体的方便,还能让@Version继续给其他账户行用,完美适配需求。
  • 如果追求极致性能,或者就是不想碰乐观锁的逻辑,再用方案2:直接SQL更新省了JPA管理实体的开销,但得自己处理审计需要的余额数据。

内容的提问来源于stack exchange,提问作者Sanjay Prajapat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 06:22:03