PostgreSQl 在创建新实体时正确锁定

ale*_*oid 2 postgresql transactions spring-transactions spring-data-jpa spring-boot

我有两个并发事务,它们检查是否存在适当的 PostgreSQL 表记录,如果不存在,则尝试插入一个新记录。

我有以下 Spring 数据存储库方法:

@Lock(LockModeType.PESSIMISTIC_WRITE)
TaskApplication findByUserAndTask(User user, Task task);
Run Code Online (Sandbox Code Playgroud)

正如你可能看到的,我已经@Lock(LockModeType.PESSIMISTIC_WRITE)在那里添加了。在我的服务方法内部,我检查实体是否存在,如果不存在,则创建一个新实体:

@Transactional
public TaskApplication createIfNotExists(User user, Task task) {
    TaskApplication taskApplication = taskApplicationRepository.findByUserAndTask(user, task);
    if (taskApplication == null) {
        taskApplication = create(user, task);
    }
}
Run Code Online (Sandbox Code Playgroud)

我还添加了对tasks_applications (user_id, task_id)字段的唯一约束。

ALTER TABLE public.task_applications 
ADD CONSTRAINT "task_applications-user_id_task_id_unique" 
UNIQUE (user_id, task_id)
Run Code Online (Sandbox Code Playgroud)

自动创建对应的唯一索引:

CREATE UNIQUE INDEX "task_applications-user_id_task_id_unique" 
ON public.task_applications 
USING btree (user_id, task_id)
Run Code Online (Sandbox Code Playgroud)

不幸的是,如果两个并发事务具有相同的user_idtask_id,第二个事务总是会失败,并出现以下异常:

Caused by: org.postgresql.util.PSQLException: ERROR: duplicate key value violates unique constraint "task_applications-user_id_task_id_unique"
  Key (user_id, task_id)=(1, 1) already exists.
Run Code Online (Sandbox Code Playgroud)

我做错了什么以及如何修复它以便能够在我的服务方法中处理这种情况?

更新

我不明白为什么以下方法不会阻止第二个事务的执行,直到第一个事务被提交或回滚:

TaskApplication taskApplication = taskApplicationRepository.findByUserAndTask(user, task);
Run Code Online (Sandbox Code Playgroud)

我不确定 PostgreSQL,但通常它应该在没有记录的情况下根据唯一索引阻止执行。

如何实现?

更新2

执行过程中生成的SQL命令序列:

select * from task_applications taskapplic0_ where taskapplic0_.user_id=? and taskapplic0_.task_id=? for update of taskapplic0_
insert into task_applications values (?,...)
Run Code Online (Sandbox Code Playgroud)

小智 5

@Lock(LockModeType.PESSIMISTIC_WRITE)
TaskApplication findByUserAndTask(User user, Task task);
Run Code Online (Sandbox Code Playgroud)

将仅在查询返回的实体上获得悲观锁(行级锁)。在结果集为空的情况下,不会获取锁,并且findByUserAndTask不会阻止事务。

有几种方法可以处理并发插入:

  1. 使用唯一索引来防止添加重复项并处理适合您的应用程序需求的异常
  2. 如果您的数据库支持,请在要插入数据的表上获取表级锁。JPA 不支持。
  3. 在新实体和专用于存储锁的表上使用行级锁来模拟表级锁。这个新表应该在您想要在插入时获得悲观锁的每个表中都有一行
    public enum EntityType {
        TASK_APPLICATION
    } 
Run Code Online (Sandbox Code Playgroud)
    @Getter
    @Entity
    public class TableLock {
        @Id
        private Long id

        @Enumerated(String)
        private EntityType entityType;
    }
Run Code Online (Sandbox Code Playgroud)
    public interface EntityTypeRepository extends Repository<TableLock, Long> {
        
        @Lock(LockModeType.PESSIMISTIC_WRITE)
        TableLock findByEntityType(EntityType entityType);
    }

Run Code Online (Sandbox Code Playgroud)

有了这样的设置,你只需要获得锁:

@Transactional
public TaskApplication createIfNotExists(User user, Task task) {
    TaskApplication taskApplication = taskApplicationRepository.findByUserAndTask(user, task);

    if (taskApplication == null) {
        findByEntityType(EntityType.TASK_APPLICATION);
        taskApplication = taskApplicationRepository.findByUserAndTask(user, task);
        
        if (taskApplication == null) {
            taskApplication = create(user, task);
        }
    }

    return taskApplication;
}
Run Code Online (Sandbox Code Playgroud)

对于大多数情况,第一种方法(唯一索引)是最好且最有效的。获取表锁(本机/模拟)很繁重,应谨慎使用。

我不确定 PostgreSQL,但通常它应该在没有记录的情况下根据唯一索引阻止执行。

索引的存在不会影响 select / insert 语句是否阻塞。这种行为是由悲观锁控制的。