微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

PostgreSQl正确锁定新实体的创建

如何解决PostgreSQl正确锁定新实体的创建

我有两个并发事务,它们检查是否存在适当的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 (?,...)

解决方法

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

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

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

  1. 使用唯一索引来防止添加重复项并处理适合您的应用程序需求的异常
  2. 如果数据库支持,请在要插入数据的表上获得表级锁。 JPA不支持它。
  3. 在专门用于存储锁的新实体和表上使用行级锁来模拟表级锁。您想要在插入时获得悲观锁的每个表上,该新表应在每个表上都有一行
    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语句是否被阻塞。这种行为是由悲观锁控制的。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。