PostgreSQl 正确锁定新实体的创建

Posted

技术标签:

【中文标题】PostgreSQl 正确锁定新实体的创建【英文标题】:PostgreSQl correct lock on creation of new entity 【发布时间】:2020-12-25 08:03:36 【问题描述】:

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

我有以下 Spring Data 存储库方法:

@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 (?,...)

【问题讨论】:

使用INSERT ... ON CONFLICT 谢谢,但请查看我更新的问题 我看到了更新,但不明白它的相关性。 它的相关性如下 - 我无法在我的服务方法上处理所描述的问题。同步锁机制不起作用。 我使用Lock(LockModeType.PESSIMISTIC_WRITE) 超过findByUserAndTask method。据我了解,它应该阻止事务#2 的执行,直到事务#1 被提交或回滚。为了存档它,它应该使用task_applications-user_id_task_id_unique 索引作为信号量,以防尝试创建新实体。但它不起作用。 【参考方案1】:
@Lock(LockModeType.PESSIMISTIC_WRITE)
TaskApplication findByUserAndTask(User user, Task task);

只会在查询返回的实体上获得悲观锁(行级锁)。在结果集为空的情况下,没有获得锁,findByUserAndTask 不会阻塞事务。

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

    使用唯一索引来防止添加重复项并根据您的应用程序需求处理异常 如果您的数据库支持,请在要插入数据的表上获取表级锁。 JPA 不支持。 在专用于存储锁的新实体和表上使用行级锁模拟表级锁。这个新表应该在每个你想在插入时获得悲观锁的表上都有一行
    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;

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

我不确定 PostgreSQL,但通常它应该阻止基于唯一索引的执行,以防没有记录。

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

【讨论】:

以上是关于PostgreSQl 正确锁定新实体的创建的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL 服务器启动失败,无法创建锁定文件:权限被拒绝

使用 Hibernate 和 HSQLDB 在数据库级别锁定实体

PostgreSQL 锁定模式

根据条件限制/锁定所有 Post/Delete Rest API 端点 [关闭]

如何在实体框架中完全锁定一行

如何为 postgresql MATERIALIZED VIEW 创建 typeorm 实体