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

Spring Data JPA实体并发访问控制:如何确保单线程独占实体?

解决多线程下Entity并发访问的竞态问题(以库存扣减为例)

这是并发场景下非常典型的竞态条件问题,尤其是库存扣减这类涉及数据一致性的操作,稍不注意就会出现“超卖”或者重复更新的情况。下面给你几种靠谱的实现方案,从简单到进阶都有,你可以根据自己的业务场景选择:

1. 乐观锁(适合低到中并发场景)

乐观锁的核心思路是“先操作,再验证”,通过给实体添加版本号字段,确保只有当数据版本和预期一致时,更新才会生效。

实现步骤:

  • 给你的Product实体类添加版本号字段,用JPA的@Version注解标记:
@Entity
public class Product {
    @Id
    private Long id;
    private Integer stock;
    @Version // 乐观锁版本字段
    private Integer version;
    
    // getter、setter、构造方法省略
}
  • 在服务层的扣减方法中,先查询产品,修改库存后保存,同时捕获乐观锁失败的异常(如果并发冲突,会抛出OptimisticLockingFailureException):
@Service
@Transactional
public class OrderService {
    @Autowired
    private ProductRepository productRepository;

    public boolean reduceStock(Long productId) {
        Product product = productRepository.findById(productId).orElseThrow(() -> new RuntimeException("产品不存在"));
        if (product.getStock() <= 0) {
            return false; // 库存不足
        }
        product.setStock(product.getStock() - 1);
        try {
            productRepository.save(product);
            return true;
        } catch (OptimisticLockingFailureException e) {
            // 并发冲突,可以选择重试或者返回失败
            return false;
        }
    }
}

优点:

  • 不需要显式加锁,性能开销小
  • 适合并发不是特别高的场景

缺点:

  • 冲突时会失败,需要业务层处理重试逻辑

2. 悲观锁(适合高并发场景)

悲观锁的思路是“先锁再操作”,在查询数据的时候就把这条记录锁住,其他线程必须等待当前线程释放锁后才能操作。

实现步骤:

  • 在你的ProductRepository中,添加一个带悲观锁的查询方法:
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE) // 写锁,阻止其他线程修改
    @Query("select p from Product p where p.id = ?1")
    Optional<Product> findByIdForUpdate(Long id);
}
  • 服务层调用这个方法进行库存扣减:
@Service
@Transactional
public class OrderService {
    @Autowired
    private ProductRepository productRepository;

    public boolean reduceStock(Long productId) {
        Product product = productRepository.findByIdForUpdate(productId).orElseThrow(() -> new RuntimeException("产品不存在"));
        if (product.getStock() <= 0) {
            return false;
        }
        product.setStock(product.getStock() - 1);
        productRepository.save(product);
        return true;
    }
}

注意事项:

  • 一定要在事务中使用悲观锁,否则锁会立即释放
  • 可以设置锁超时时间,避免长时间占用锁(比如用@Lock结合@QueryHints设置javax.persistence.lock.timeout)
  • 要注意锁的粒度,避免锁住过多数据导致性能问题

3. 数据库原子更新(性能最优方案)

上面两种方案都是“先读再写”,而直接用数据库的原子更新语句,可以把读和写合并成一步,利用数据库的原子性保证不会出现竞态。

实现步骤:

  • 在ProductRepository中定义一个自定义更新方法,直接执行SQL扣减库存:
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Modifying
    @Query("update Product p set p.stock = p.stock - 1 where p.id = ?1 and p.stock > 0")
    int reduceStockById(Long productId);
}
  • 服务层调用这个方法,根据返回的更新行数判断是否成功:
@Service
@Transactional
public class OrderService {
    @Autowired
    private ProductRepository productRepository;

    public boolean reduceStock(Long productId) {
        int affectedRows = productRepository.reduceStockById(productId);
        return affectedRows > 0; // 返回1表示更新成功,0表示库存不足或产品不存在
    }
}

优点:

  • 性能最好,减少了一次查询操作
  • 完全依赖数据库的原子性,不会出现竞态条件
  • 代码逻辑更简洁

4. 分布式锁(多实例部署场景)

如果你的服务是多节点部署的,上面的数据库锁可能无法跨节点生效,这时候需要用到分布式锁,确保同一时间只有一个节点的线程能操作该产品。

实现示例(以Redisson为例):

  • 先引入Redisson依赖,配置好Redis连接
  • 服务层代码:
@Service
public class OrderService {
    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private RedissonClient redissonClient;

    public boolean reduceStock(Long productId) {
        String lockKey = "product:stock:lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试获取锁,最多等待5秒,持有锁10秒
            if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
                // 这里可以用上面的任意一种数据库更新方式
                int affectedRows = productRepository.reduceStockById(productId);
                return affectedRows > 0;
            } else {
                // 获取锁失败,返回重试提示或者失败
                return false;
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

注意事项:

  • 要确保锁的粒度足够细(比如按产品ID加锁),避免影响其他产品的操作
  • 要处理锁超时的问题,防止因为线程异常导致锁无法释放

内容的提问来源于stack exchange,提问作者nagy.zsolt.hun

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 09:25:07