Warm tip: This article is reproduced from serverfault.com, please click

sql-Spring Data JPA插入到多个表中以避免锁定表

(sql - Spring data jpa insert into multiple tables to avoid locking tables)

发布于 2020-11-28 11:24:47

你能帮我了解如何有效地将实体插入多个表吗?

我有3个表和3个实体。Pricebook具有一系列SKU,每个SKU具有1个价格。基本上,我要以事务方式插入多个实体,如果有约束,我必须更新链中的实体。

在此处输入图片说明

一旦尝试向数据库中并行插入多个Pricebook,就出现了问题,因此我实际上正在捕获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;

       //....
    }

这是Upbook的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> {}
Questioner
Alejandro Kolio
Viewed
11
crizzis 2020-12-01 05:17:11

我相信你可能会遇到此问题,尤其是在两个事务都尝试插入相同的SKU时,这一问题很有可能发生。

如果是这样,我可以考虑两种缓解方法:

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

  2. 完整的解决方案:如果你希望两个事务都成功,则必须为该SKU获取一个表级锁你应该可以使用自定义更新查询来执行此操作:

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

然后,只需将该方法作为内部的第一个操作调用即可createSku请注意,这可能仅比将事务放入队列稍有效率,因此,如果我是你,我可能仍会采用这种方法。

编辑我也不太了解为你提供两次事务冲突的一致结果的确切方案,这是你要并行化的批量插入类型的东西吗?如果你真的并行运行事务,则可以对输入集进行分组,以使SKU不会重叠。或者,将重复数据删除并插入Skus中。就像我说的,我不知道用例是什么,所以不确定这是否有意义。