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
更像是一个查找表,两个事务中的实体是否共享任何sku
s?此外,这将有助于查看每个事务执行的实际查询,因为这样我们可以推断正在获取哪些锁
您可以尝试通过在PESSIMISTIC_WRITE
模式中为您的服务方法可能更新的任何实体预先获取锁(作为代码中的第一个查询)来解决问题。但是,如果没有看到代码和/或查询,我们将无法提供任何进一步的帮助
@crizzis 当然,谢谢。我添加了 3 种方法,来自 3 种服务。 Pricebook 为每个 sku 代码调用 SkuService.createSkus() 以保存或更新,并在其中调用 PriceService.createPrice() 相应地创建价格。
【参考方案1】:
我相信您可能会遇到this issue,这很有可能,尤其是如果两个交易都尝试插入相同的 SKU。
如果是这种情况,我可以想出两种方法来缓解它:
部分解决方案:尝试按sku.code
对List<CreateSkuRequest> skus
中的SKU 进行排序,并(如果这还不够)使用saveAndFlush()
来存储它们,确保插入顺序。这应该消除循环等待,这意味着现在至少有一个事务应该成功(另一个可能会违反唯一约束)
完整解决方案:如果您希望两个事务都成功,则必须为SKU
表获取表级锁。您应该能够使用自定义更新查询来做到这一点:
@Query(value = "LOCK TABLE SKU IN EXCLUSIVE MODE", nativeQuery = true)
@Modifying
void lockTable();
然后,只需调用该方法作为createSku
中的第一个操作。请注意,这可能只会比将事务放入队列中效率稍高一些,所以如果我是你,我可能仍然会采用这种方法。
编辑我也不太了解确切的场景,它会给你两个事务冲突的一致结果,这是你试图并行化的批量插入类型的东西吗?如果您真的一心想要并行运行事务,也许您可以对输入集进行分组,这样 SKU 就不会重叠。或者,删除重复数据并预先插入 Sku
s。正如我所说,我不知道用例是什么,所以不确定这是否有意义。
【讨论】:
谢谢您,我会尝试您的解决方案并返回结果 看来你是对的,锁定比在队列中更有效。谢谢,对我有帮助 如果其中一个事务失败(由于某种异常),您是否需要手动释放锁或以某种方式自动完成?这部分是如何工作的? 锁定仅在事务上下文中才有意义。一旦事务因为某种原因回滚,锁显然就被释放了以上是关于Spring data jpa 插入多个表以避免锁定表的主要内容,如果未能解决你的问题,请参考以下文章