在MySQL Hibernate JPA事务期间未检测到死锁

thy*_*yzz 9 java mysql hibernate jpa database-deadlocks

警告!!!TL; DR

MySQL 5.6.39  
mysql:mysql-connector-java:5.1.27
org.hibernate.common:hibernate-commons-annotations:4.0.5.Final  
org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.0.Final  
org.hibernate:hibernate-core:4.3.6.Final
org.hibernate:hibernate-entitymanager:4.3.6.Final  
org.hibernate:hibernate-validator:5.0.3.Final
Run Code Online (Sandbox Code Playgroud)

HTTP方法:POST,API路径:/ reader

实体“ 读者 ”引擎:innoDB

id
name
total_pages_read
Run Code Online (Sandbox Code Playgroud)

类映射:

@Entity
@Table(name = "reader")
public class Reader{
    @Column(name = "id")
    private Long id;

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

    @Column(name = "total_pages_read")
    private Long total_pages_read;

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "reader", orphanRemoval = true)
    private Set<Book_read> book_reads;

    ...
}
Run Code Online (Sandbox Code Playgroud)

我在Reader写入服务类中使用方法createEntity()和recalculateTotalPageRead():

@Service
public class ReaderWritePlatformServiceJpaRepositoryImpl{
    private final ReaderRepositoryWrapper readerRepositoryWrapper;

    ...

    @Transactional
    public Long createEntity(final Long id, final String name, final Long total_pages_read){
        try {
            final Reader reader = new Reader(id, name, total_pages_read);
            this.readerRepositoryWrapper.saveAndFlush(reader);

            return 1l;
        } catch (final Exception e) {
            return 0l;
        }
    }

    ...
}
Run Code Online (Sandbox Code Playgroud)

HTTP方法:POST,API路径:/ bookread

实体“ book_read ”引擎:innoDB

id  
reader_id  
book_title  
number_of_pages 
Run Code Online (Sandbox Code Playgroud)

类映射:

@Entity
@Table(name = "book_read")
public class Book_read{
    @Column(name = "id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "reader_id")
    private Reader reader;

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

    @Column(name = "number_of_pages")
    private Long number_of_pages;

    ...
}
Run Code Online (Sandbox Code Playgroud)

我使用方法,createEntity()recalculateTotalPageRead()Book_read write服务类中:

@Service
public class Book_readWritePlatformServiceJpaRepositoryImpl{
    private final ReaderRepositoryWrapper readerRepositoryWrapper;
    private final Book_readRepositoryWrapper bookReadRepositoryWrapper;

    ...

    @Transactional
    public Long createEntity(final Long id, final Long reader_id, final String book_title, final Long number_of_pages){
        try {
            final Reader reader = this.readerRepositoryWrapper.findOneWithNotFoundDetection(reader_id);

            final Book_read book_read = new Book_read(id, reader, book_title, number_of_pages);
            this.bookReadRepositoryWrapper.saveAndFlush(book_read);

            this.recalculateTotalPageRead(reader);

            return 1l;
        } catch (final Exception e) {
            return 0l;
        }
    }

    private void recalculateTotalPageRead(final Reader reader){
        Long total_pages_read =  Long.valueOf(0);
        Set<Book_read> book_reads = reader.getBook_reads();
        for (Book_read book_read : book_reads){
            total_pages_read += book_read.getNumber_of_pages();
        }

        reader.setTotal_pages_read(total_pages_read);
        this.readerRepositoryWrapper.saveAndFlush(reader);
    }

    ...
}
Run Code Online (Sandbox Code Playgroud)

当我尝试创建两个实体时:

样本“ 读者 ”:

id | name       | total_pages_read
-----------------------------------
1  | Foo Reader | 0(by default)
Run Code Online (Sandbox Code Playgroud)

示例“ book_read ”:2个单独的POST方法调用

id | reader_id | book_title | number_of_pages 
---------------------------------------------
1  | 1         | Foo Book   | 2
2  | 1         | Bar Book   | 3
Run Code Online (Sandbox Code Playgroud)

如上例所示,在创建“ book_read ” -s 后,预期对实体“ reader ”的更改:

样本阅读器:

id | name       | total_pages_read
-----------------------------------
1  | Foo Reader | 5
Run Code Online (Sandbox Code Playgroud)

但是,从什么我已经经历刚好有3案件同时创造者2“ book_read ”记录并发

情况1(确定):

  • 第一个“ book_read ”完成创建
  • 将任何现有的“ 读者 ” id 1的“ book_read ” 获取到“ book_reads ” 列表中。“ book_reads ”大小= 1。
  • 添加NUMBER_OF_PAGES每个“的book_read ”在列表中total_pages_read “的读者 ” ID 1.当前total_pages_read = 2。
  • 开始创建第二个“ book_read
  • 第二个“ book_read ”完成创建
  • 将任何现有的“ 读者 ” id 1的“ book_read ” 获取到“ book_reads ” 列表中。“ book_reads ”大小= 2。
  • 将列表中每个“ book_read ”的number_of_pages添加到“ reader ” id 1的total_pages_read中。
  • 最终结果:total_pages_read = 5。

情况2(确定):

  • 事务1)开始创建第一个“ book_read
  • 事务2)开始创建第二个“ book_read
  • 事务1)第一个“ book_read ”完成创建
  • 事务2)第二个“ book_read ”完成创建
  • 事务1)将“ 读者 ” id 1的任何现有“ book_read ” 获取到“ book_reads ” 列表中。“ book_reads ”大小= 1。
  • 事务2)将“ 读者 ” id 1的任何现有“ book_read ” 获取到“ book_reads ” 列表中。“ book_reads ”大小= 1。
  • 事务1)添加NUMBER_OF_PAGES每个“的book_read列表中的”到total_pages_read “的读取器 ” ID 1.当前total_pages_read = 2。
  • 事务2)将列表中每个“ book_read ”的number_of_pages添加到“ reader ” id 1的total_pages_read中引发死锁异常
  • 重试(事务2)开始创建第二个“ book_read
  • 事务2)第二个“ book_read ”完成创建
  • 事务2)将“ 读者 ” id 1的任何现有“ book_read ” 获取到“ book_reads ” 列表中。“ book_reads ”大小= 2。
  • 将列表中每个“ book_read ”的number_of_pages添加到“ reader ” id 1的total_pages_read中。
  • 最终结果:total_pages_read = 5。

情况3(不确定):

  • 第一个“ book_read ”完成创建
  • 将任何现有的“ 读者 ” id 1的“ book_read ” 获取到“ book_reads ” 列表中。“ book_reads ”大小= 1。
  • 添加NUMBER_OF_PAGES每个“的book_read ”在列表中total_pages_read “的读者 ” ID 1.当前total_pages_read = 2。
  • 开始创建第二个“ book_read
  • 第二个“ book_read ”完成创建
  • 将任何现有的“ 读者 ” id 1的“ book_read ” 获取到“ book_reads ” 列表中。“ book_reads ”大小= 1。
  • 添加NUMBER_OF_PAGES每个“的book_read ”在列表中total_pages_read “的读者 ” ID 1.当前total_pages_read = 3,未检测到死锁
  • 最终结果:total_pages_read = 3。

如何解决案例3?

干杯,快乐编程:D

Nes*_*kil 3

您所经历的称为丢失更新,这实际上不是 JPA 级别的问题,您可以在 MySQL shell 中轻松重现此问题。我假设您没有对数据库本身进行任何更改,因此您的默认事务隔离级别是REPEATABLE READ

在 MySQL 中,REPEATABLE READ不会检测可能丢失的更新(尽管这是对此隔离级别的普遍理解)。您可以在 SO和评论线程上查看此答案以了解更多信息。

基本上通过使用 MVCC,MySQL 试图避免争用和死锁。在你的情况下,你将不得不做出权衡并选择牺牲一些速度来保持一致性。

您的选择是使用SELECT ... FOR UPDATE语句或设置更严格的隔离级别SERIALIZABLE(您可以对单个事务执行此操作)。这两个选项都会阻止读取,直到并发事务提交/回滚。因此,稍后(或很久以后,取决于应用程序的要求),您将看到数据的一致视图。

您还可以在这里这里这里阅读相关内容。

并发很难。:)

更新:考虑下面的评论后,实际上您还有另一个选择:为您的数据模型实现乐观锁定。JPA对此有支持,请查看此处此处。您实现的效果基本相同,但采用了一些不同的方法(您将必须重新启动失败的事务以达到不匹配的版本),并且由于锁定较少而导致争用较少。