1. 概述

在本篇文章中,我们将探讨如何在 Spring Data JPA 中为自定义查询方法以及预定义的 Repository CRUD 方法启用事务锁。

同时会介绍不同类型的锁机制,以及如何设置锁等待超时时间。

2. 锁类型

JPA 定义了两种主要的锁机制:悲观锁(Pessimistic Locking)乐观锁(Optimistic Locking)

2.1. 悲观锁

当我们使用悲观锁时,事务一旦访问某个实体,该实体就会立即被锁定。事务通过提交或回滚来释放锁。

悲观锁适用于并发写操作频繁的场景,能有效防止数据冲突,但代价是性能开销较大。

2.2. 乐观锁

乐观锁不会立即锁定实体。通常的做法是给实体添加一个版本号(version),在更新时进行版本比对。

⚠️ 如果多个事务同时修改同一个实体,当版本号不一致时,JPA 实现会抛出 OptimisticLockException 异常,并回滚当前事务。

除了版本号,也可以使用时间戳、哈希值或校验和等方式实现乐观锁机制。

3. 在查询方法上启用事务锁

✅ 我们可以通过在 Repository 的查询方法上添加 @Lock 注解,并传入所需的锁模式,来为实体获取锁。

LockModeType 是一个枚举类型,用于指定锁的类型,Spring Data JPA 会将该锁模式传递给数据库,从而对实体加锁。

3.1. 自定义查询方法加锁

例如,为一个自定义查询方法添加乐观锁:

@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@Query("SELECT c FROM Customer c WHERE c.orgId = ?1")
List<Customer> fetchCustomersByOrgId(Long orgId);

3.2. 对预定义方法加锁

我们也可以为 Spring Data JPA 提供的默认方法(如 findById())添加锁机制,只需在 Repository 中声明并加上 @Lock 注解:

@Lock(LockModeType.PESSIMISTIC_READ)
Optional<Customer> findById(Long customerId);

⚠️ 注意

  • 如果没有开启事务就尝试加锁,JPA 会抛出 TransactionRequiredException
  • 如果无法获取锁但未导致事务回滚,会抛出 LockTimeoutException

4. 设置事务锁等待超时时间

✅ 在使用悲观锁时,数据库会尝试立即加锁。如果加锁失败,JPA 会抛出 LockTimeoutException

为了避免这种情况,我们可以通过 @QueryHints 注解设置锁等待超时时间:

@Lock(LockModeType.PESSIMISTIC_READ)
@QueryHints({@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000")})
Optional<Customer> findById(Long customerId);

上面的例子中,设置了 3000 毫秒的等待时间。如果 3 秒内无法获取锁,才会抛出异常。

📌 更多关于锁超时设置的细节可以参考 ObjectDB 官方文档

5. 总结

✅ 在高并发场景下,合理使用事务锁可以有效保障数据一致性。

  • 悲观锁适合对数据一致性要求极高、并发写操作频繁的场景。
  • 乐观锁则适用于并发读操作较多、可以接受最终一致性的场景。

📌 示例代码已上传至 GitHub,欢迎查阅。


小贴士

  • 实际开发中,优先考虑业务场景选择合适的锁策略。
  • 事务中加锁要谨慎,避免死锁或性能瓶颈。
  • 使用 @Lock 时务必确保方法被事务管理器管理(即加上 @Transactional 注解)。

原始标题:Enabling Transaction Locks in Spring Data JPA