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
注解)。