【问题标题】:How to handle Concurrent create if not exists with spring data JPA如果spring数据JPA不存在,如何处理并发创建
【发布时间】:2021-10-26 07:22:59
【问题描述】:

我在我的 java-springboot 项目中使用 spring data JPA。我有一种方法可以处理许多相互关联的实体(其中一些是延迟加载的),并且该方法由多个线程同时运行。我的问题是 - 当说线程 T1 来并尝试为作者创建一本新书时,它会检查数据库中是否已经存在书籍信息并开始创建新记录,同时线程 T2 也尝试相同和开始创建相同的资源,但是在持久化时,它得到 DataIntegrtyViolation 异常。

@Entity
@Table(name = "BookInfo", uniqueConstraints = @UniqueConstraint(columnNames = { 
"publisherName",
    "bookTitle", "authorName" }), indexes = {
            @Index(name = "BookInfoProviderIndex", columnList = "publisherName") 
})
public class BookInfo  {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY/* , generator="table_gen" */)
  private Long id;

  @Column(name = "publisherName", nullable = false)
  private String publisherName;

  @Column(name = "bookTitle")
  private String bookTitle;

  @Column(name = "authorName")
  private String authorName;

存储库 -

@Repository
public interface BookInfoRepository extends PagingAndSortingRepository<BookInfo, Long> {

public BookInfo findFirstByPublisherNameAndBookTitleAndAuthorName(String publisherName,
        String bookTitle, String authorName);

}

以及导致问题的方法 -

@Service
public class AuthorService{
  @Autowired
  BookInfoRepository bookRepo;

  @Transactional
  public createAutoBiography(AuthorDTO dto){
      BookInfo book = null;
      book = bookRepo.findFirstByPublisherNameAndBookTitleAndAuthorName(dto.getPubName(), dto.getTitle(), dto.author());
      if(book != null){
         book = buildBookInfo(dto);
         book = bookRepo.save(book);
      }

      ...//other methods to create dependent resources for Author class and save the entities using the respective repositories 
  }
}

我尝试了以下 -

  1. findFirstByPublisherNameAndBookTitleAndAuthorName() repo 方法上添加了@Lock(PESSIMISTIC_WRITE) - 这有效,但冲突不会非常频繁,我担心它会对整体功能产生性能影响,以处理上述偶尔发生的冲突.
  2. 使用 @Transactional(isolation = Isolation.SERIALIZABLE, propagation = Propagation.REQUIRES_NEW) 将书籍创建部分作为单独的方法 - 但我没有收到任何会话错误
  3. 使 BookInfo 存储库实现 JpaRepository 而不是 PagingAndSortingRepository 以使用 saveAndFlush() 而不是 save(),但看起来即使 saveAndFlush 也不会提交到 DB,提交只会隐式发生在外部方法的事务结束时

另外,随机注意到 Author 对象本身的资源创建存在冲突,由于并发,它不是将新遇到的 BookInfo 添加到现有作者,而是使用新书信息为作者创建一个新条目

一般来说,使 JPA 事务线程安全的最佳方法是什么?

【问题讨论】:

  • 如果您想在代码中处理它,请锁定 bookId,创建一个具有 bookId 的对象,并且一次只能调用一个方法实例。方法也可以检查书是否已经存在。如果要在 DB 中处理,请在 DB 中使用并发级别。我认为你可以在代码中处理事情可能是。
  • 我对锁的关注是性能,如果我的理解有误,请纠正我,但是放置一个悲观的写锁意味着不给任何其他线程/方法对表的任何读写访问权限,我的问题想要处理,不会那么普遍,但肯定会存在,所以为了解决这个问题,我不希望性能受到重大影响
  • 为这三个字段添加唯一索引。
  • @zysaaa 必须已经存在唯一索引 - 否则不会抛出 DataIntegrtyViolation
  • @zysaaa 我已经对实体的 3 个字段有唯一约束 - @UniqueConstraint(columnNames = { "publisherName","bookTitle", "authorName" })

标签: java spring-boot jpa concurrency spring-data-jpa


【解决方案1】:

使用@Transactional(propagation = Propagation.REQUIRES_NEW) 找到了解决方案(您需要spring框架提供的@Transactional,而不是javax.transaction.Transactional)。 它对我不起作用的原因是由于对 Spring Data JPA 如何管理事务代理的非常不直观的警告。 考虑一下 -

@Service
public class AuthorService {

@Transactional
public void createAutoBiography() {
    createBook();
    // process other entities
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createBook() {
    // ...
}

}

在这里,您会期望创建两个事务,一个由 createAutoBiography() 创建,该事务由 createBook() 方法中创建的另一个事务暂停 - 这是我的假设错误的地方。

Spring 创建了一个事务性的 AuthorService 代理,但是一旦我们进入 AuthorService 类并调用其他内部方法,就不再涉及代理了。这意味着,不会创建新事务。 这意味着第二种方法中的任何异常都会关闭整个服务的事务,并且在尝试下一个 jpa 操作时出现 No session 错误。

因此通过将 createBook() 方法放在单独的服务类中并添加类级别的 @Transactional(propagation = Propagation.REQUIRES_NEW) 注释来解决此问题。

现在 createBook() 执行发生在一个单独的事务中,并在此方法返回(事务结束)时立即将其写入 DB,而 createAutoBiography() 中的原始事务被挂起。因此,现在如果我们捕获异常(如上面@slauth 所述)或使用某种数据库锁定,我们可以分别处理/避免 DataInegrityViolation 异常。另外,即使没有处理异常,也只有第二个事务应该回滚,而主事务仍然可以执行(不过需要测试一下)。

参考 - https://www.marcobehler.com/guides/spring-transaction-management-transactional-in-depth#transactional-pitfalls

【讨论】:

  • 我不明白这将如何使您免于插入违反唯一约束的书籍......
  • 是的,仅此还不够,必须将创建逻辑包含在 DB 锁块中,早些时候该块也已到位,但由于并发性和整个方法包装在事务块中,碰撞非常频繁。现在有了一个单独的事务上下文,书籍在服务方法返回后立即被保存,并且锁确保书籍属性的相同组合只被写入一次
【解决方案2】:

您可以简单地捕获DataIntegrityViolation 并再次调用findFirstByPublisherNameAndBookTitleAndAuthorName(或createAutoBiography)。

【讨论】:

  • 是的,这是我遇到问题时的第一个想法,但我似乎记得我在某些地方读到过,依靠异常处理来解决这个问题并不是最佳解决方案——尽管我个人认为与异常处理相比,使用单独的 txns 或锁对性能的影响更大(因为冲突会减少)
猜你喜欢
  • 2013-07-16
  • 2020-12-27
  • 2020-10-10
  • 2019-07-30
  • 1970-01-01
  • 2023-03-25
  • 2012-06-29
  • 2012-11-26
  • 1970-01-01
相关资源
最近更新 更多