MySQL Hibernate JPA 事务中未检测到死锁

9

警告!!! 省略长篇大论

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

实体"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;

    ...
}

我在一个读者写服务类中使用了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);
    }

    ...
}

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

样例 "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"之后,期望对实体"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" 的大小为 1。
  • 将列表中每个 "book_read" 的 number_of_pages 添加到 "reader" ID 为 1 的 total_pages_read 中。当前的 total_pages_read 为 2。
  • 开始创建第二个 "book_read"
  • 第二个 "book_read" 创建完成
  • 将 "reader" 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) 将"reader"id为1的现有"book_read"添加到名为"book_reads"的列表中。 "book_reads"大小= 1。
  • (事务2) 将"reader"id为1的现有"book_read"添加到名为"book_reads"的列表中。 "book_reads"大小= 1。
  • (事务1) 将列表中每个"book_read"的number_of_pages添加到"reader" id 1的total_pages_read中。 当前的total_pages_read=2。
  • (事务2) 将列表中每个"book_read"的number_of_pages添加到"reader" id 1的total_pages_read中。发生死锁异常
  • 重试(事务2)开始创建第二个"book_read"
  • (事务2) 完成创建第二个"book_read"
  • (事务2) 将"reader"id为1的现有"book_read"添加到名为"book_reads"的列表中。"book_reads"大小= 2。
  • 将列表中每个"book_read"的number_of_pages添加到"reader" id 1的total_pages_read中。
  • 最终结果: total_pages_read = 5。

Case 3 (不OK):

  • 第1个 "book_read" 创建完成。
  • 获取id为1的"reader"现有的任何"book_read",将其存入一个名为"book_reads"的列表中。"book_reads"的大小为1。
  • 将列表中每个"book_read"的number_of_pages添加到id 1的"reader"的total_pages_read中。当前total_pages_read=2。
  • 开始创建第2个 "book_read"。
  • 第2个 "book_read"创建完成。
  • 获取id为1的"reader"现有的任何"book_read",将其存入一个名为"book_reads"的列表中。"book_reads"的大小为1。
  • 将列表中每个"book_read"的number_of_pages添加到id 1的"reader"的total_pages_read中。当前total_pages_read=3。未检测到死锁。
  • 最终结果: total_pages_read = 3。

我该如何解决第3个案例?

干杯! 愉快的编程:D

已解决!

在数据模型上实现乐观锁定。

@Entity
@Table(name = "reader")
public class Reader{
    @Version
    @Column(name = "version")
    private int version;
    
    @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;

    ...
}

听起来你需要让你的Java函数启动一个包装的SQL事务,因为没有它,Hibernate只会生成没有任何事务内容的SQL,并且不受可重复读约束的限制。total_pages_read更新机制使用了什么SQL? - danblack
@danblack 对不起,我之前在我的createEntity()方法上忘记了添加@Transactional注解。我已经将它们添加到代码片段中了。谢谢你的指出。顺便说一下,我真的不明白你上一个关于更新机制的问题。附言:我对Java EE还是很新,感谢你的包容 :D - thyzz
显示生成的 SQL。 - Rick James
你在MySQL中使用哪个隔离级别?是REPEATABLE READ吗? - Nestor Sokil
2个回答

3
你所经历的情况被称为“丢失更新”,这并不是JPA级别的问题,你可以在MySQL shell中轻松重现。我假设你没有对数据库本身进行任何更改,所以你的默认事务隔离级别是“可重复读”。
在MySQL中,“可重复读”不会检测可能的丢失更新(尽管这是这种隔离级别的常见理解)。你可以查看SO上的这个答案和评论线程以了解更多信息。
基本上,通过使用MVCC,MySQL试图避免争用和死锁。在你的情况下,你必须进行权衡,并选择为了一致性而牺牲一些速度。
你的选择是使用“SELECT ... FOR UPDATE”语句或设置更严格的隔离级别,即“串行化”(你可以为单个事务执行此操作)。这两个选项都会阻塞读取,直到并发事务提交/回滚。因此,你将看到数据的一致视图,只是稍后(或者根据应用程序的要求,可能很长时间)。

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

并发编程很难。 :)

更新:在考虑下面的评论后,您实际上还有另一种选择:为数据模型实现乐观锁定。 JPA对此提供了支持,请参见这里这里。您所实现的基本上是相同的,但采用了稍微不同的方法(您将不得不重新启动由于版本不匹配而失败的事务),并且由于较少的锁定而减少争用。


MySQL是悲观锁,而乐观锁则是像Galera这样在提交时失败的东西。 - danblack
@danblack 基本上,MVCC是乐观锁定方法的扩展。我不确定这样说是否在技术上正确,但这是我的一般看法。 - Nestor Sokil
1
MVCC是一个概念,可以采用乐观或悲观的方法来实现。InnoDB独占锁和MyISAM表锁是悲观冲突预防的例子。相比之下,Galera不会注意其他节点上的事务,乐观地假定事务将成功。只有在提交时才进行认证以验证这种乐观假设。 - danblack
@danblack 先生,您是正确的。我混淆了OL和OCC(乐观并发控制),这是两个不同的概念。 - Nestor Sokil

-1

我觉得你遇到的问题是由于外键关系的属性锁定受到了关系的拥有方的控制。

由于book_reads集合被注解为@OneToMany(mappedBy = "reader"),它不控制锁定,而是由另一方控制,这意味着在更新集合时会得到两个完全独立的锁定,它们互相不认识。

删除Book_read.readermappedBy注解就可以解决这个问题。

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

还可以参考Vlad Mihalcea关于这个主题的文章:https://vladmihalcea.com/hibernate-collections-optimistic-locking/


javax.persistence仅为@OneToMany提供了mappedBy。正如我所解释的,有一种情况是检测到锁定。或者是我的理解有误?请给我更多启示。谢谢。 - thyzz
你说得对,嗯。我猜这就只剩下将其变为单向关系的选择了。我已经相应地更新了我的答案。 我不认为死锁是某些部分工作的迹象,正如你所暗示的那样,而是某些坏事情出现了问题。 我认为问题在于锁在不同的对象上以不同的顺序发生。所有的锁定应该由我认为的“聚合根”——Reader来控制。 - Jens Schauder

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接