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

问题描述 投票:9回答:2

警告!!! 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之后期望对实体“reader”的更改如上例所示:

样本阅读器:

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

但是从我一直遇到的情况来看,同时创建这两个“book_read”记录的情况恰好是3个案例:

案例1(好):

  • 第一本“book_read”完成了创作
  • 将“reader”id 1的任何现有“book_read”添加到列表“book_reads”中。 “book_reads”size = 1。
  • 将列表中每个“book_read”的number_of_pages添加到“reader”id的total_pages_read 1.当前total_pages_read = 2。
  • 开始创建第二个“book_read”
  • 第二个“book_read”完成了创作
  • 将“reader”id 1的任何现有“book_read”添加到列表“book_reads”中。 “book_reads”size = 2。
  • 将列表中每个“book_read”的number_of_pages添加到“reader”id 1的total_pages_read中。
  • 最终结果:total_pages_read = 5。

案例2(确定):

  • (事务1)开始创建第一个“book_read”
  • (交易2)开始创建第二个“book_read”
  • (交易1)第1次“book_read”完成创建
  • (交易2)第2次“book_read”完成创作
  • (事务1)将“reader”id 1的任何现有“book_read”获取到列表“book_reads”中。 “book_reads”size = 1。
  • (事务2)将“reader”id 1的任何现有“book_read”获取到列表“book_reads”中。 “book_reads”size = 1。
  • (事务1)将列表中每个“book_read”的number_of_pages添加到“reader”id的total_pages_read 1.当前total_pages_read = 2。
  • (事务2)将列表中每个“book_read”的number_of_pages添加到“reader”id的total_pages_read 1.死锁异常抛出。
  • 重试(事务2)开始创建第二个“book_read”
  • (交易2)第2次“book_read”完成创作
  • (事务2)将“reader”id 1的任何现有“book_read”获取到列表“book_reads”中。 “book_reads”size = 2。
  • 将列表中每个“book_read”的number_of_pages添加到“reader”id 1的total_pages_read中。
  • 最终结果:total_pages_read = 5。

案例3(不行):

  • 第一本“book_read”完成了创作
  • 将“reader”id 1的任何现有“book_read”添加到列表“book_reads”中。 “book_reads”size = 1。
  • 将列表中每个“book_read”的number_of_pages添加到“reader”id的total_pages_read 1.当前total_pages_read = 2。
  • 开始创建第二个“book_read”
  • 第二个“book_read”完成了创作
  • 将“reader”id 1的任何现有“book_read”添加到列表“book_reads”中。 “book_reads”size = 1。
  • 将列表中每个“book_read”的number_of_pages添加到“reader”id的total_pages_read 1.当前total_pages_read = 3.未检测到死锁。
  • 最终结果:total_pages_read = 3。

我如何解决案例3?

干杯,快乐的节目:D

java mysql hibernate jpa database-deadlocks
2个回答
3
投票

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

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

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

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

你也可以阅读这个hereherehere

并发很难。 :)

更新:在考虑下面的评论之后,实际上还有另一种选择:为您的数据模型实施乐观锁定。 JPA对此有所支持,请查看herehere。您实现的基本相同,但采用了一种不同的方法(您必须重新启动未能使版本不匹配的事务)并减少由于锁定较少而导致的争用。


-1
投票

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

由于book_reads集合使用@OneToMany(mappedBy = "reader")进行注释,因此它不会控制锁定,因此另一方面会这样做,这意味着在更新集合时会获得两个单独的锁定,而这些锁定并不能真正识别彼此。

删除Book_read.readermappedBy注释应该解决这个问题。

所有这些实际上都适用于带有版本属性的乐观锁定,这是推荐的做事方式。

另见Vlad Mihalcea关于这个主题的文章:https://vladmihalcea.com/hibernate-collections-optimistic-locking/

© www.soinside.com 2019 - 2024. All rights reserved.