PostgreSQl 正确锁定创建新实体

PostgreSQl correct lock on creation of new entity

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

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

@Lock(LockModeType.PESSIMISTIC_WRITE)
TaskApplication findByUserAndTask(User user, Task task);

如您所见,我在此处添加了 @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);
    }
}

我还在 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)

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

CREATE UNIQUE INDEX "task_applications-user_id_task_id_unique" 
ON public.task_applications 
USING btree (user_id, task_id)

不幸的是,在两个具有相同 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.

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

已更新

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

TaskApplication taskApplication = taskApplicationRepository.findByUserAndTask(user, task);

我不确定 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 (?,...)
@Lock(LockModeType.PESSIMISTIC_WRITE)
TaskApplication findByUserAndTask(User user, Task task);

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

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

  1. 使用唯一索引来防止添加重复项并根据您的应用程序需要适当地处理异常
  2. 如果您的数据库支持,请在要插入数据的 table 上获取 table-level 锁。 JPA 不支持它。
  3. 在新实体上使用 row-level 锁模拟 table-level 锁,并且 table 专用于存储锁。这个新 table 每个 table 应该有一行,你想在插入
  4. 上获得悲观锁
    public enum EntityType {
        TASK_APPLICATION
    } 
    @Getter
    @Entity
    public class TableLock {
        @Id
        private Long id

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

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

@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;
}

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

I'm not sure about PostgreSQL, but typically it should block the execution based on unique index in case of no record.

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