Spring Data JPA 对共享锁和独占锁的支持

简介

本文将会带你了解 Spring Data JPA 对锁的支持,以及如何在检索时使用共享锁或者独占锁。

JPA LockModeType

JPA 提供了几种加锁的选项,可以在获取实体时通过 LockModeType 枚举来指定。

如下:

  • LockModeType.PESSIMISTIC_READ 可对相关表记录加共享锁或读锁。
  • LockModeType.PESSIMISTIC_WRITE 可对相关表记录加速独占锁或写锁。

如果底层数据库不支持共享锁,那么 PESSIMISTIC_READ 策略将退回到 PESSIMISTIC_WRITE,因为该选项已被广泛支持。

根据 ID 获取实体时加锁

JPA EntityManager 提供了一个 find 方法的重载,可以传递 LockModeType 采参数。用于在获取实体时上锁:

Post post = entityManager.find(Post.class, id, lockMode);

Spring Data JpaRepository 未提供此选项,但我们可以使用自定义 Spring Data Repository 来实现。

例如,可以让 PostRepository 继承 JpaRepositoryCustomPostRepository

@Repository
public interface PostRepository
        extends JpaRepository<Post, Long>, CustomPostRepository {
}

In the CustomPostRepository interface, we can define a lockById method like this:

CustomPostRepository 接口中,可以这样定义 lockById 方法:

public interface CustomPostRepository {
 
    Post lockById(Long id, LockModeType lockMode);
}

Spring Data JPA 可以为查询方法或使用 @Query 注解的方法提供实现,而对于我们的 CustomPostRepository 接口,我们必须提供 CustomPostRepositoryImpl 实现,Spring Data JPA 在运行时创建 PostRepository Java 对象实例时将使用该实现。

CustomPostRepositoryImpl 如下:

public class CustomPostRepositoryImpl
        implements CustomPostRepository {
     
    @PersistenceContext
    private EntityManager entityManager;
 
    @Override
    public Post lockById(Long id, LockModeType lockMode) {
        return entityManager.find(Post.class, id, lockMode);
    }
}

实现了 lockById 方法后,进行测试:

PostgreSQL 数据库上加共享锁,必须使用 LockModeType.PESSIMISTIC_READ

Post postWithSharedLock = postRepository.lockById(
    1L,
    LockModeType.PESSIMISTIC_READ
);

然后,Hibernate 将执行以下 SQL 查询:

SELECT
    p.id AS id1_0_0_,
    p.slug AS slug2_0_0_,
    p.title AS title3_0_0_
FROM
    post p
WHERE
    p.id = 1
FOR SHARE

要获取独占锁,可以s使用 PESSIMISTIC_WRITE 选项:

Post postWithExclusiveLock = postRepository.lockById(
    2L,
    LockModeType.PESSIMISTIC_WRITE
);

Hibernate 会使用 FOR UPDATE 子句来代替:

SELECT
    p.id AS id1_0_0_,
    p.slug AS slug2_0_0_,
    p.title AS title3_0_0_
FROM
    post p
WHERE
    p.id = 2
FOR UPDATE

查询实体记录时加锁

还可以对给定 SQL 查询返回的所有记录加行级锁,为此 Spring Data JPA 提供了 @Lock 注解。

例如,在 PostCommentRepository 中,可以定义 lockAllByPostId 方法,对与给定 Post 父记录相关联的所有子 PostComment 行应用读取锁:

@Repository
public interface PostCommentRepository
        extends JpaRepository<PostComment, Long> {
 
    @Query("""
        select pc
        from PostComment pc
        where pc.post.id = :postId
        """)
    @Lock(LockModeType.PESSIMISTIC_READ)
    List<PostComment> lockAllByPostId(
        @Param("postId") Long postId
    );
}

执行 lockAllByPostId 方法:

List<PostComment> commentWithLock = postCommentRepository.lockAllByPostId(1L);

Hibernate 将在 PostgreSQL 上运行以下 SQL 查询:

SELECT
    pc.id as id1_1_,
    pc.post_id as post_id3_1_,
    pc.review as review2_1_
FROM
    post_comment pc
WHERE
    pc.post_id = 1
FOR SHARE OF pc

参考:https://vladmihalcea.com/spring-data-jpa-locking/