Spring data jpa 插入多个表以避免锁定表

Posted

技术标签:

【中文标题】Spring data jpa 插入多个表以避免锁定表【英文标题】:Spring data jpa insert into multiple tables to avoid locking tables 【发布时间】:2021-03-10 22:16:39 【问题描述】:

能否请您帮助我了解如何有效地将实体插入到多个表中?

相应地,我有 3 个表和 3 个实体。 Pricebook 有一系列 SKU,每个 SKU 有 1 个价格。基本上我想要的是在事务中插入多个实体,以防出现约束,我必须更新链中的实体。

当我尝试将超过 1 个价格手册并行插入 DB 时,就会出现问题,因此我实际上遇到了 PostgreSQL 死锁。我发现一种解决方法是将它们一个一个地插入队列中,但我知道这不是一个好主意。

这可能是一个愚蠢的问题,之前已经回答过,但我希望有人能给我提示。

    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Table(name = "pricebook")
    public class Pricebook 
       @Id
       @GeneratedValue(strategy=GenerationType.AUTO) 
       private Long id;
       //....
    

    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Table(name = "sku")
    public class Sku 
       @Id
       @GeneratedValue(strategy=GenerationType.AUTO)
       private Long id;
       //....
    

    @Entity
    @NoArgsConstructor
    @AllArgsConstructor
    @Table(name = "price")
    public class Price 
       @Id
       @GeneratedValue(strategy=GenerationType.AUTO)
       private Long id;

       @JoinColumn(name = "pricebook_id", referencedColumnName = "id", unique = true)
       @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
       private Pricebook pricebook;

       @JoinColumn(name = "sku_id", referencedColumnName = "id", unique = true)
       @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
       private Sku sku;

       //....
    

这是 upsert 的 PricebookService 逻辑。

  @NonNull
    @Transactional
    public Pricebook createPricebook(@NonNull CreatePricebookRequest request) 
        final Instant startDate = PricebookConverter.toDate(request.getStartDate());
        final Instant expirationDate = PricebookConverter.toDate(request.getExpirationDate());

        if (startDate.isAfter(expirationDate)) 
            throw new InvalidParametersException("The pricebook's start date later then its expiration date.");
        

        final Region region = regionService.findRegionByName(request.getRegion());

        final Optional<Pricebook> isPricebookFound =
                pricebookRepository.findByRegionAndPricebookTypeAndStartDateAndExpirationDate(region,
                        request.getPricebookName(), startDate, expirationDate);

        final Pricebook savedOrUpdatedPricebook;
        if (isPricebookFound.isPresent()) 
            final Pricebook foundPricebook = isPricebookFound.get();

            savedOrUpdatedPricebook = pricebookRepository.save(
                    new Pricebook(foundPricebook.getPricebookId(), request.getName(), foundPricebook.getPricebookName(), foundPricebook.getRegion(), foundPricebook.getStartDate(),
                            foundPricebook.getExpirationDate());

            logger.info("pricebook is updated successfully, pricebook=", savedOrUpdatedPricebook);
         else 
            savedOrUpdatedPricebook = pricebookRepository.save(
                    new Pricebook(request.getName(), request.getPricebookType(), region, startDate, expirationDate);

            logger.info("pricebook is created successfully, pricebook=", savedOrUpdatedPricebook);
        

        final List<Sku> skus = skuService.createSku(savedOrUpdatedPricebook, request.getSkus());
        logger.debug("skus are saved successfully, skus=", skus);
        return savedOrUpdatedPricebook;
    

这是一个 upsert 的 SkuService 逻辑。 skuToCreateOrUpdate 基本上只是一个包装逻辑的方法,如果它被发现或新的并返回一个新对象。

    @NonNull
    public List<Sku> createSku(@NonNull Pricebook pricebook, @NonNull List<CreateSkuRequest> skus) 
        return skus.stream().map(sku -> 
            final Optional<Sku> foundSku = skuRepository.findByCode(sku.getCode());

            final Sku savedOrUpdatedSku = skuRepository.save(skuToCreateOrUpdate(sku, foundSku.map(Sku::getSkuId).orElse(null)));

            final List<Price> prices = priceService.createPrices(pricebook, savedOrUpdatedSku, sku.getPrice());
            logger.debug("prices are saved successfully, prices=", prices);
            return savedOrUpdatedSku;
        ).collect(toList());
    

这是 upsert 的 PriceService 逻辑。

    @NonNull
    public List<Price> createPrices(@NonNull Pricebook pricebook, @NonNull Sku sku, @NonNull CreatePriceRequest price) 
        final Optional<Price> foundPrice = priceRepository.findByPricebookAndSku(pricebook, sku);

        final Price savedOrUpdatedPrice;
        if (foundPrice.isPresent()) 
            final Price priceToUpdate = foundPrice.get();
            savedOrUpdatedPrice = priceRepository.save(
                    new Price(priceToUpdate.getPriceId(),
                            pricebook,
                            sku);
            logger.info("price is updated successfully, price=", savedOrUpdatedPrice);
         else 
            savedOrUpdatedPrice = priceRepository.save(
                    new Price(pricebook, sku);
            logger.info("price is created successfully, price=", savedOrUpdatedPrice);
        

        return Collections.singletonList(savedOrUpdatedPrice);
    

我到处都在使用 JpaRepository。就这样..

@Repository
public interface PricebookRepository extends JpaRepository<Pricebook, Long> 

@Repository
public interface SkuRepository extends JpaRepository<Sku, Long> 

@Repository
public interface PriceRepository extends JpaRepository<Price, Long> 

【问题讨论】:

是否有错误消息告诉您为什么您遇到了死锁?我的理解是,如果你在两个事务中运行相同的实体创建操作,你不应该遇到死锁,因为插入的顺序是相同的,所以没有循环等待。请发布服务方法,并可能启用 SQL 日志记录,以查看哪些语句已死锁 是的,当然,错误消息看起来很像。如下所示:ERROR: deadlock detected Detail: Process 27988 waits for ShareLock on transaction 1996705435; blocked by process 27990. Process 27990 waits for ShareLock on transaction 1996705089; blocked by process 27988. Hint: See server log for query details. Where: while inserting index tuple (153,8) in relation "sku"; nested exception is org.postgresql.util.PSQLException: ERROR: deadlock detected 能否请您发布用于插入Pricebook 的代码?你提到了关于更新插入和约束的事情,所以我想有一些条件逻辑?我也理解sku更像是一个查找表,两个事务中的实体是否共享任何skus?此外,这将有助于查看每个事务执行的实际查询,因为这样我们可以推断正在获取哪些锁 您可以尝试通过在PESSIMISTIC_WRITE 模式中为您的服务方法可能更新的任何实体预先获取锁(作为代码中的第一个查询)来解决问题。但是,如果没有看到代码和/或查询,我们将无法提供任何进一步的帮助 @crizzis 当然,谢谢。我添加了 3 种方法,来自 3 种服务。 Pricebook 为每个 sku 代码调用 SkuService.createSkus() 以保存或更新,并在其中调用 PriceService.createPrice() 相应地创建价格。 【参考方案1】:

我相信您可能会遇到this issue,这很有可能,尤其是如果两个交易都尝试插入相同的 SKU。

如果是这种情况,我可以想出两种方法来缓解它:

    部分解决方案:尝试按sku.codeList&lt;CreateSkuRequest&gt; skus 中的SKU 进行排序,并(如果这还不够)使用saveAndFlush() 来存储它们,确保插入顺序。这应该消除循环等待,这意味着现在至少有一个事务应该成功(另一个可能会违反唯一约束)

    完整解决方案:如果您希望两个事务都成功,则必须为SKU 表获取表级锁。您应该能够使用自定义更新查询来做到这一点:

@Query(value = "LOCK TABLE SKU IN EXCLUSIVE MODE", nativeQuery = true)
@Modifying
void lockTable();

然后,只需调用该方法作为createSku 中的第一个操作。请注意,这可能只会比将事务放入队列中效率稍高一些,所以如果我是你,我可能仍然会采用这种方法。

编辑我也不太了解确切的场景,它会给你两个事务冲突的一致结果,这是你试图并行化的批量插入类型的东西吗?如果您真的一心想要并行运行事务,也许您可​​以对输入集进行分组,这样 SKU 就不会重叠。或者,删除重复数据并预先插入 Skus。正如我所说,我不知道用例是什么,所以不确定这是否有意义。

【讨论】:

谢谢您,我会尝试您的解决方案并返回结果 看来你是对的,锁定比在队列中更有效。谢谢,对我有帮助 如果其中一个事务失败(由于某种异常),您是否需要手动释放锁或以某种方式自动完成?这部分是如何工作的? 锁定仅在事务上下文中才有意义。一旦事务因为某种原因回滚,锁显然就被释放了

以上是关于Spring data jpa 插入多个表以避免锁定表的主要内容,如果未能解决你的问题,请参考以下文章

Spring Data JPA - 并发批量插入/更新

Spring data Jpa,Mybatis,读写锁,@Lock 使用

Spring-data-jpa:批量插入不起作用

Spring Data Jpa如何实现审计和乐观锁功能

Spring Data Jpa如何实现审计和乐观锁功能

Spring Data JDBC 中的乐观锁