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

时间:2019-03-18 05:06:14

标签: 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

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

实体“ 阅读器”引擎:innoDB

id
name
total_pages_read

类映射:

@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;

    ...
}

我在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;
        }
    }

    ...
}

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

实体“ book_read ”引擎:innoDB

id  
reader_id  
book_title  
number_of_pages 

类映射:

@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;

    ...
}

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

@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);
    }

    ...
}

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

阅读器”示例:

id | name       | total_pages_read
-----------------------------------
1  | Foo Reader | 0(by default)

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

id | reader_id | book_title | number_of_pages 
---------------------------------------------
1  | 1         | Foo Book   | 2
2  | 1         | Bar Book   | 3

按上述示例创建“ book_read ”-s后,预计实体“ 阅读器”的更改:

样本读取器:

id | name       | total_pages_read
-----------------------------------
1  | Foo Reader | 5

但是根据我的经验,在同时创建这两个“ book_read ”记录时,恰好有3种情况同时

案例1(确定):

  • 第一个“ book_read ”创建完成
  • 将任何现有的“ 阅读器” ID 1的“ book_read ”放入列表“ book_reads ”。 “ book_reads ”的大小=1。
  • 将列表中每个“ book_read ”的 页数 添加到“ 阅读器< / strong>” ID1。当前 total_pages_read =2。
  • 开始创建第二个“ book_read
  • 第二个“ book_read ”创建完成
  • 将任何现有的“ 阅读器” ID 1的“ book_read ”放入列表“ book_reads ”。 “ book_reads ”的大小=2。
  • 将列表中每个“ book_read ”的 页数 添加到 total_pages_read 阅读器”的ID1。
  • 最终结果: 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 )将列表中每个“ book_read ”的 页数 添加到 < 的“ 阅读器”中的em> total_pages_read 。 =2。
  • 事务2 )将列表中每个“ book_read ”的 页数 添加到 < “ 阅读器” ID的em> total_pages_read 引发死锁异常
  • 重试(事务2 )开始创建第二个“ book_read
  • 交易2 )第二个“ book_read ”创建完成
  • 交易2 )将ID为1的任何现有的“ 阅读器”的“ book_read ”放入列表“ book_reads ”。 “ book_reads ”的大小=2。
  • 将列表中每个“ book_read ”的 页数 添加到 total_pages_read 阅读器”的ID1。
  • 最终结果: total_pages_read =5。

情况3(不确定):

  • 第一个“ book_read ”创建完成
  • 将任何现有的“ 阅读器” ID 1的“ book_read ”放入列表“ book_reads ”。 “ book_reads ”的大小=1。
  • 将列表中每个“ book_read ”的 页数 添加到“ 阅读器< / strong>” ID1。当前 total_pages_read =2。
  • 开始创建第二个“ book_read
  • 第二个“ book_read ”创建完成
  • 将任何现有的“ 阅读器” ID 1的“ book_read ”放入列表“ book_reads ”。 “ book_reads ”的大小=1。
  • 将列表中每个“ book_read ”的 页数 添加到 total_pages_read 阅读器”的ID。1.当前 total_pages_read =3。未检测到死锁
  • 最终结果: total_pages_read =3。

如何解决案例3?

干杯, 快乐编程:D

2 个答案:

答案 0 :(得分:3)

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

在MySQL中,REPEATABLE READ不会检测到可能丢失的更新(即使这是对该隔离级别的共识)。您可以查看this answer on SO和评论主题以了解更多信息。

基本上,通过使用MVCC,MySQL尝试避免争用和死锁。在您的情况下,您将不得不权衡并选择为了一致性而牺牲一些速度。

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

您也可以阅读此hereherehere

并发很难。 :)

更新:在考虑了以下评论之后,实际上您还有另一个选择:为数据模型实现乐观锁定。 JPA为此提供支持,请查看herehere。您获得的结果基本上是相同的,但是方法有所不同(您将不得不重新启动版本不匹配的事务),并且由于锁定较少,争用也会减少。

答案 1 :(得分:-1)

我认为您面临的问题是FK关系的属性锁定由关系的拥有一方控制。

由于book_reads的集合带有@OneToMany(mappedBy = "reader")的注释,因此它无法控制锁定,而另一侧则可以控制,这意味着您在更新集合时会得到两个单独的锁,它们无法真正识别彼此

删除Book_read.readermappedBy注释应该可以解决此问题。

所有这些实际上都适用于带有version属性的乐观锁定,这是无论如何都建议这样做。

另请参见弗拉德·米哈尔西娅(Vlad Mihalcea)撰写的有关以下主题的文章:https://vladmihalcea.com/hibernate-collections-optimistic-locking/