MySQL Hibernate JPA 事务期间未检测到死锁
Deadlock not detected during MySQL Hibernate JPA transaction
警告!!! 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
实体“reader”引擎:innoDB
id
name
total_pages_read
Class映射:
@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() class:
@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
Class映射:
@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 写入服务 class:
中使用方法 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”会发生变化,如上例所示:
样本Reader:
id | name | total_pages_read
-----------------------------------
1 | Foo Reader | 5
但是根据我的经历,在创建这 2 个“book_read”记录时恰好有 3 个案例并发:
案例 1(正常):
- 第一个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=1.
- 添加 number_of_pages 每个“book_read”在“reader”的 total_pages_read 列表中,id 1。当前 total_pages_read = 2.
- 开始创建第二个“book_read”
- 第 2 个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=2.
- 添加 number_of_pages 每个“book_read”在列表中 total_pages_read 的“reader” id 1。
- 最终结果:total_pages_read = 5.
案例 2(正常):
- (事务 1) 开始创建第一个“book_read”
- (事务 2) 开始创建第二个“book_read”
- (事务 1) 第一个“book_read”完成创建
- (事务 2) 第二个“book_read”创建完成
- (事务 1) 获取“[=447=”的任何现有“book_read” ]”将 id 1 放入列表“book_reads”。 “book_reads”尺寸=1.
- (事务 2) 获取“[=447=”的任何现有“book_read” ]”将 id 1 放入列表“book_reads”。 “book_reads”尺寸=1.
- (事务 1) 添加 number_of_pages 每个“book_read”列表中的total_pages_read of “ reader" id 1. 当前 total_pages_read = 2.
- (事务 2) 添加 number_of_pages 每个“book_read”列表中的total_pages_read of “ reader" id 1。抛出死锁异常。
- 重试(事务 2)开始创建第二个“book_read”
- (事务 2) 第二个“book_read”创建完成
- (事务 2) 获取“[=447=”的任何现有“book_read” ]”将 id 1 放入列表“book_reads”。 “book_reads”尺寸=2.
- 添加 number_of_pages 每个“book_read”在列表中 total_pages_read 的“reader” id 1。
- 最终结果:total_pages_read = 5.
情况 3(不正确):
- 第一个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=1.
- 添加 number_of_pages 每个“book_read”在“reader”的 total_pages_read 列表中,id 1。当前 total_pages_read = 2.
- 开始创建第二个“book_read”
- 第 2 个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=1.
- 添加 number_of_pages 每个“book_read”在列表中 total_pages_read 的“reader” id 1。当前 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;
...
}
我认为您面临的问题是 FK 关系的属性锁定由关系的 owning 方控制。
由于 book_reads
集合用 @OneToMany(mappedBy = "reader")
注释,它不控制锁,所以另一方控制锁,这意味着你在更新集合时得到两个单独的锁,它们并不真正识别彼此。
删除 Book_read.reader
和 mappedBy
注释应该可以解决这个问题。
所有这些实际上都适用于具有版本属性的乐观锁定,无论如何这是推荐的做事方式。
另请参阅 Vlad Mihalcea 撰写的关于该主题的文章:https://vladmihalcea.com/hibernate-collections-optimistic-locking/
您遇到的问题称为丢失更新,这确实不是 JPA 级别的问题,您可以在 MySQL shell 中轻松重现此问题.我假设您没有对数据库本身进行任何更改,因此您的默认事务隔离级别是 REPEATABLE READ
.
在 MySQL 中,REPEATABLE READ
不检测可能丢失的更新(即使这是对此隔离级别的普遍理解)。您可以查看 this answer on SO 和评论线程以了解更多信息。
基本上通过使用 MVCC,MySQL 试图避免争用和死锁。在您的情况下,您将不得不做出权衡并选择为了一致性而牺牲一些速度。
您的选择是使用 SELECT ... FOR UPDATE
语句或设置更严格的隔离级别,即 SERIALIZABLE
(您可以对单个事务执行此操作)。这两个选项都将阻止读取,直到并发事务 commit/rollback。因此,您将看到一致的数据视图,只是稍后(或更晚,取决于应用程序的要求)。
并发很难。 :)
更新:在考虑了下面的评论之后,您实际上还有另一个选择:为您的数据模型实施乐观锁定。 JPA对此有支持,请看here and here。您实现的结果基本相同,但方法略有不同(您将不得不重新启动失败的事务到不匹配的版本)并且由于更少的锁定而减少争用。
警告!!! 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
实体“reader”引擎:innoDB
id
name
total_pages_read
Class映射:
@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() class:
@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
Class映射:
@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 写入服务 class:
中使用方法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”会发生变化,如上例所示:
样本Reader:
id | name | total_pages_read
-----------------------------------
1 | Foo Reader | 5
但是根据我的经历,在创建这 2 个“book_read”记录时恰好有 3 个案例并发:
案例 1(正常):
- 第一个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=1.
- 添加 number_of_pages 每个“book_read”在“reader”的 total_pages_read 列表中,id 1。当前 total_pages_read = 2.
- 开始创建第二个“book_read”
- 第 2 个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=2.
- 添加 number_of_pages 每个“book_read”在列表中 total_pages_read 的“reader” id 1。
- 最终结果:total_pages_read = 5.
案例 2(正常):
- (事务 1) 开始创建第一个“book_read”
- (事务 2) 开始创建第二个“book_read”
- (事务 1) 第一个“book_read”完成创建
- (事务 2) 第二个“book_read”创建完成
- (事务 1) 获取“[=447=”的任何现有“book_read” ]”将 id 1 放入列表“book_reads”。 “book_reads”尺寸=1.
- (事务 2) 获取“[=447=”的任何现有“book_read” ]”将 id 1 放入列表“book_reads”。 “book_reads”尺寸=1.
- (事务 1) 添加 number_of_pages 每个“book_read”列表中的total_pages_read of “ reader" id 1. 当前 total_pages_read = 2.
- (事务 2) 添加 number_of_pages 每个“book_read”列表中的total_pages_read of “ reader" id 1。抛出死锁异常。
- 重试(事务 2)开始创建第二个“book_read”
- (事务 2) 第二个“book_read”创建完成
- (事务 2) 获取“[=447=”的任何现有“book_read” ]”将 id 1 放入列表“book_reads”。 “book_reads”尺寸=2.
- 添加 number_of_pages 每个“book_read”在列表中 total_pages_read 的“reader” id 1。
- 最终结果:total_pages_read = 5.
情况 3(不正确):
- 第一个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=1.
- 添加 number_of_pages 每个“book_read”在“reader”的 total_pages_read 列表中,id 1。当前 total_pages_read = 2.
- 开始创建第二个“book_read”
- 第 2 个“book_read”创建完成
- 将“reader”id 1 的任何现有“book_read”放入列表“book_reads”。 “book_reads”尺寸=1.
- 添加 number_of_pages 每个“book_read”在列表中 total_pages_read 的“reader” id 1。当前 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;
...
}
我认为您面临的问题是 FK 关系的属性锁定由关系的 owning 方控制。
由于 book_reads
集合用 @OneToMany(mappedBy = "reader")
注释,它不控制锁,所以另一方控制锁,这意味着你在更新集合时得到两个单独的锁,它们并不真正识别彼此。
删除 Book_read.reader
和 mappedBy
注释应该可以解决这个问题。
所有这些实际上都适用于具有版本属性的乐观锁定,无论如何这是推荐的做事方式。
另请参阅 Vlad Mihalcea 撰写的关于该主题的文章:https://vladmihalcea.com/hibernate-collections-optimistic-locking/
您遇到的问题称为丢失更新,这确实不是 JPA 级别的问题,您可以在 MySQL shell 中轻松重现此问题.我假设您没有对数据库本身进行任何更改,因此您的默认事务隔离级别是 REPEATABLE READ
.
在 MySQL 中,REPEATABLE READ
不检测可能丢失的更新(即使这是对此隔离级别的普遍理解)。您可以查看 this answer on SO 和评论线程以了解更多信息。
基本上通过使用 MVCC,MySQL 试图避免争用和死锁。在您的情况下,您将不得不做出权衡并选择为了一致性而牺牲一些速度。
您的选择是使用 SELECT ... FOR UPDATE
语句或设置更严格的隔离级别,即 SERIALIZABLE
(您可以对单个事务执行此操作)。这两个选项都将阻止读取,直到并发事务 commit/rollback。因此,您将看到一致的数据视图,只是稍后(或更晚,取决于应用程序的要求)。
并发很难。 :)
更新:在考虑了下面的评论之后,您实际上还有另一个选择:为您的数据模型实施乐观锁定。 JPA对此有支持,请看here and here。您实现的结果基本相同,但方法略有不同(您将不得不重新启动失败的事务到不匹配的版本)并且由于更少的锁定而减少争用。