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

Spring Boot RESTful API中Hibernate引发的竞态条件问题求助

解决Spring Boot+Hibernate并发竞态条件问题

看起来你遇到了典型的并发读写不一致问题——多个请求同时操作同一对用户的余额和交易记录,导致读取旧值后执行插入/更新,破坏了业务逻辑的原子性。我来帮你拆解问题根源,然后给出几个靠谱的解决方案:

为什么之前的方案无效?

  • synchronized块:它是JVM进程级别的锁,如果你的应用是多实例部署(比如集群),不同实例的请求会直接绕开锁;就算是单实例,若事务提交在synchronized块之外,锁释放后其他线程可能在事务提交前读到旧数据,依然会有问题。
  • 常规事务隔离级别:比如REPEATABLE READ(MySQL默认)会读取事务内的数据快照,无法保证读取到数据库最新的行数据;而且单纯靠隔离级别无法跨表保证操作的串行性。

可行解决方案

1. 数据库悲观锁(最直接的单/多实例通用方案)

核心思路:在读取BalanceEntity时,给对应的行加排他锁,其他事务必须等待锁释放才能读取该行,确保后续的更新/插入操作基于最新值执行。

修改你的balanceDAO.getByBothUsersId方法,通过Hibernate添加悲观写锁:

public BalanceEntity getByBothUsersId(Integer payerId, Integer payeeId) {
    Session session = entityManager.unwrap(Session.class);
    return session.createQuery(
            "from BalanceEntity where payerId = :payerId and payeeId = :payeeId", 
            BalanceEntity.class)
            .setParameter("payerId", payerId)
            .setParameter("payeeId", payeeId)
            .setLockMode(LockMode.PESSIMISTIC_WRITE) // 添加排他锁
            .uniqueResult();
}

同时确保你的服务层方法在同一个事务内完成所有操作(你已经加了类级别的@Transactional,保持默认的REQUIRED传播行为即可)。锁会在事务提交/回滚时自动释放,保证整个读-改-插流程的原子性。

2. 乐观锁(适合并发量不高的场景)

如果你的并发请求不是特别频繁,可以用乐观锁来避免悲观锁的性能开销。给BalanceEntity添加版本字段,每次更新时校验版本号:

第一步,给实体类加@Version注解:

@Entity
public class BalanceEntity {
    // 其他字段...
    @Version
    private Integer version; // 版本号,Hibernate自动维护递增

    // getter、setter
}

第二步,在updatePendingAmount方法中,Hibernate会自动校验版本号:如果当前线程读取的版本号和数据库中的版本号不一致,会抛出OptimisticLockingFailureException,这时候你需要捕获异常并重试整个操作。可以用Spring的RetryTemplate来实现重试逻辑:

@Autowired
private RetryTemplate retryTemplate;

public ResponseDTO insert(RequestParams request) {
    return retryTemplate.execute(context -> {
        // 原来的业务逻辑:读余额、更新、插入交易
        // ...
    });
}

3. 分布式锁(多实例部署必选)

如果你的应用是多实例集群部署,synchronized和数据库行锁可能不足以完全避免跨实例的并发问题,这时候需要用分布式锁来锁定用户对(payerId+payeeId),保证同一对用户的操作全局串行执行。

比如用Redis的Redisson实现分布式锁:

@Autowired
private RedissonClient redissonClient;

public ResponseDTO insert(RequestParams request) {
    String lockKey = String.format("balance-lock:%d:%d", request.getPayerId(), request.getPayeeId());
    RLock lock = redissonClient.getLock(lockKey);
    
    try {
        // 尝试获取锁,5秒等待超时,30秒持有超时防止死锁
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            // 在锁内执行完整的业务逻辑(读余额、更新、插入交易)
            // ...
        } else {
            throw new RuntimeException("当前操作繁忙,请稍后重试");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("操作被中断");
    } finally {
        // 确保锁被释放
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

注意:分布式锁要和数据库事务配合使用,最好是先获取锁,再开启事务,避免锁释放后事务还未提交导致其他线程读到旧数据。

额外优化建议

  • 确保transactionDao.insert和balanceDAO.update操作都在同一个事务内,避免部分提交的情况。
  • 避免在事务内执行耗时操作(比如远程调用),防止锁持有时间过长影响性能。
  • 针对高频操作的用户对,可以考虑在业务层做本地缓存+分布式锁的组合优化,但要注意缓存一致性问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:54:54