Spring Boot JPA项目For循环内更新数据未提交、余额更新异常问题
问题根因
@Transactional注解对私有方法不生效:Spring声明式事务基于AOP动态代理实现,只有public方法被类外部的调用者调用时,事务切面才会生效。你代码里的callToDeductBalanceAmount、callToUpdateBalanceAmount都是private方法,方法上的@Transactional完全不生效,导致每次Repository调用都单独开启短事务,更新完就提交,但JPA的持久化上下文(一级缓存)没有同步更新。
- JPQL更新语句不会自动同步一级缓存:你用自定义
@Modifying注解的UPDATE语句直接操作数据库,执行后只会修改数据库的记录,不会更新JPA一级缓存里缓存的Wallet实体对象。第二次循环查询余额时,JPA直接返回了缓存中第一次查询到的余额为1000rs的旧实体,没有去数据库拉取最新值,所以第二次扣减是基于1000计算得到800rs,覆盖了第一次的更新结果。
- JPQL更新语句不会自动同步一级缓存:你用自定义
修复方案
1. 修正事务配置
将需要事务控制的方法改为public,且避免同个类内部调用事务方法,确保AOP代理能切入事务逻辑。如果一定要在同类调用,需要通过代理对象调用,比如注入自身代理或者用AopContext.currentProxy()获取代理对象后再调用对应方法。
// 示例:修正方法修饰符 @Service public class WalletService { @Override public String doSomething(List<RequestDTO> requestDTO) { // 调用public的事务方法,触发代理切入 return callToDeductBalanceAmount(requestDTO); } // 改为public,事务生效 @Transactional(rollbackFor = Exception.class) public String callToDeductBalanceAmount(List<RequestDTO> requestDTO) { // 原有逻辑不变 } }
2. 解决一级缓存不一致问题
两种方案二选一即可:
- 方案一:放弃自定义UPDATE语句,改用JPA实体状态更新。查询到Wallet实体后直接修改余额属性,JPA会自动同步缓存和数据库,不需要手动写UPDATE语句:
for (RequestDTO lRequest : requestDTO) { double requestAmount = lRequest.getRequestAmount(); Wallet wallet = walletRepository.checkForAvailableBalance(...); if(null != wallet) { // 直接修改实体属性 wallet.setBalanceAmount(wallet.getBalanceAmount() - requestAmount); // 保存后缓存自动同步,下次查询就是最新值 walletRepository.save(wallet); } }
- 方案二:保留UPDATE语句,执行后手动清空一级缓存,让下一次查询强制走数据库拉取最新值。可以在Repository的UPDATE方法上加上
clearAutomatically = true属性:
@Modifying(clearAutomatically = true) @Transactional @Query("UPDATE Wallet w SET w.balanceAmount = :updatedAmount WHERE ...") Integer setUpdatedBalanceAmount(..., @Param("updatedAmount") double updatedAmount);
3. 补充并发控制(推荐)
当前逻辑没有并发控制,若同时有多个转账请求,仍会出现余额计算错误的问题。可以通过加悲观锁或乐观锁规避:
- 悲观锁:查询余额时添加行锁,防止其他事务同时修改
@Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT w FROM Wallet w WHERE ...") Wallet checkForAvailableBalance(...);
- 乐观锁:给Wallet实体加
@Version字段,更新时自动校验版本,冲突时抛出异常重试即可。
内容的提问来源于stack exchange,提问作者Superman
相关产品推荐
相关产品推荐

