Spring JPA + Hibernate + Postgres 无法实现"FOR UPDATE OF"查询,如何非原生SQL方式仅锁定目标表实现悲观写锁?
看起来你遇到了Hibernate默认会对查询中加载的所有实体(包括关联表)加悲观锁的问题,明明加了org.hibernate.lockMode.ps=NONE的Hint却没生效,对吧?我来给你几个不用原生SQL的解决思路:
问题根源
Hibernate的@Lock(LockModeType.PESSIMISTIC_WRITE)注解默认会对查询结果中包含的所有实体(包括通过JOIN FETCH加载的关联实体)加锁,哪怕你给关联表加了lockMode的Hint,在你使用的Hibernate 6.x版本里,可能因为查询解析的优先级问题,这个Hint没有被正确应用,导致额外生成了对payment_status表的锁语句。
解决方案1:自定义Repository实现,手动配置LockOptions
这是最可靠的方式,通过直接操作EntityManager来构建查询,明确指定只锁定主表,跳过关联表的锁:
步骤1:定义自定义Repository接口
先创建一个包含目标方法的自定义接口:
public interface PaymentCustomRepository { Stream<Payment> getPaymentsStream(List<String> statuses); }
步骤2:实现自定义Repository
编写实现类,用EntityManager构建查询并设置锁选项:
@Repository public class PaymentCustomRepositoryImpl implements PaymentCustomRepository { private final EntityManager entityManager; // 构造注入EntityManager public PaymentCustomRepositoryImpl(EntityManager entityManager) { this.entityManager = entityManager; } @Override public Stream<Payment> getPaymentsStream(List<String> statuses) { // 构建JPQL查询,保留JOIN FETCH避免N+1问题 TypedQuery<Payment> query = entityManager.createQuery( """ SELECT p FROM Payment p JOIN FETCH p.status ps WHERE ps.name IN :statuses """, Payment.class ) .setParameter("statuses", statuses); // 设置悲观写锁,并指定仅锁定主表,关联表ps不锁定 query.setLockMode(LockModeType.PESSIMISTIC_WRITE); // 转换为Hibernate的Query对象,配置LockOptions query.unwrap(org.hibernate.query.Query.class) .setLockOptions(new LockOptions(LockMode.PESSIMISTIC_WRITE) // 给关联表的别名ps设置NONE锁,不锁定它 .setAliasSpecificLockMode("ps", LockMode.NONE) // 设置SKIP LOCKED,对应你原来的JAKARTA_LOCK_TIMEOUT=-2 .setTimeOut(LockOptions.SKIP_LOCKED)); return query.getResultStream(); } }
步骤3:让原Repository继承自定义接口
修改你的PaymentRepository,继承刚才的自定义接口,这样就能调用这个方法了:
public interface PaymentRepository extends JpaRepository<Payment, Long>, PaymentCustomRepository { // 其他原有方法 }
这个方式会让Hibernate生成你期望的SQL:
SELECT p1_0.*, s1_0.id, s1_0.name FROM payment p1_0 JOIN payment_status s1_0 ON s1_0.id = p1_0.status_id WHERE s1_0.name IN(?,?) FOR UPDATE OF p1_0 SKIP LOCKED
解决方案2:调整查询方式,避免JOIN FETCH(备选)
如果你能接受延迟加载关联的PaymentStatus,可以去掉JOIN FETCH,改用批量加载优化N+1问题,这样Hibernate只会锁定Payment表:
@Lock(LockModeType.PESSIMISTIC_WRITE) @QueryHints({ @QueryHint(name = AvailableSettings.JAKARTA_LOCK_TIMEOUT, value = "-2"), // 批量加载PaymentStatus,减少N+1查询 @QueryHint(name = "jakarta.persistence.fetchgraph", value = "Payment.statusBatchFetch") }) @Query(value = """ SELECT p FROM Payment p WHERE p.status.name IN :statuses """) Stream<Payment> getPaymentsStream(@Param("statuses") List<String> statuses);
然后在Payment实体上定义批量加载的实体图:
@NamedEntityGraph(name = "Payment.statusBatchFetch", attributeNodes = @NamedAttributeNode(value = "status", subgraph = "statusBatch"), subgraphs = @NamedSubgraph(name = "statusBatch", attributeNodes = @NamedAttributeNode("name"))) @Entity // 其他实体注解 public class Payment implements Serializable { // 实体字段 }
不过这个方式不如第一种直接,因为还是依赖延迟加载的配置,而自定义Repository的方式能更精准控制锁的范围。
为什么原来的Hint没生效?
在Hibernate 6.x中,org.hibernate.lockMode.<alias>这个Hint的优先级可能低于查询解析时的实体加载逻辑,当你用JOIN FETCH加载关联实体时,Hibernate默认会对所有加载的实体加锁,忽略了这个Hint。而通过setAliasSpecificLockMode直接设置LockOptions,是直接告诉Hibernate对指定别名的实体不施加锁,优先级更高,所以能生效。
备注:内容来源于stack exchange,提问作者ahgpoug

