记一次Spring Data JPA死锁分析
笔者公司目前使用的ORM为Spring Data JPA,其底层基于Hibernate,Hibernate是一个重量级的ORM框架,在不了解Hibernate机制的情况下使用Spring Data JPA时,可能会遇到很多比较奇怪的问题。笔者最近在公司的业务中就碰到了一个有关死锁的奇怪问题:Repository中的find查询语句竟然引发了死锁。
还原死锁现场
程序死锁日志
截取部分程序日志如下:
[ WARN ] SqlExceptionHelper:137 - SQL Error: 1213, SQLState: 40001
[ ERROR ] SqlExceptionHelper:142 - Deadlock found when trying to get lock; try restarting transaction
org.springframework.dao.CannotAcquireLockException: could not execute statement; SQL [n/a]; nested exception is org.hibernate.exception.LockAcquisitionException: could not execute statement
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:287) ~[spring-orm-5.1.9.RELEASE.jar!/:5.1.9.RELEASE]
...
at com.sun.proxy.$Proxy185.findByProductId(Unknown Source) ~[?:?]
at com.*.*.service.impl.OrderServiceImpl.transferOrReleaseLock(OrderServiceImpl.java:1086) ~[classes!/:1.3.0-SNAPSHOT]
at com.*.*.service.impl.OrderServiceImpl.manualComplete(OrderServiceImpl.java:1078) ~[classes!/:1.3.0-SNAPSHOT]
at com.*.*.service.impl.OrderServiceImpl$$FastClassBySpringCGLIB$$7a54b92d.invoke(<generated>) ~[classes!/:0.3.0-SNAPSHOT]
...
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123) ~[mysql-connector-java-8.0.17.jar!/:8.0.17]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.17.jar!/:8.0.17]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.17.jar!/:8.0.17]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953) ~[mysql-connector-java-8.0.17.jar!/:8.0.17]
...
程序代码
对应OrderServiceImpl
发生错误处的代码:
List<Order> orders = orderRepo.findByProductId(productId);
MySQL死锁日志
使用show engine innodb status
命令查看死锁信息,死锁信息在其LATEST DETECTED DEADLOCK
部分:
*** (1) TRANSACTION:
TRANSACTION 9139673, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 2
MySQL thread id 35014, OS thread handle 140228539574016, query id 65585521 172.17.1.2 user updating
update product set ... where id=*
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7035 page no 9 n bits 120 index PRIMARY of table `test`.`product` trx id 9139673 lock_mode X locks rec but not gap waiting
Record lock, heap no 50 PHYSICAL RECORD: n_fields 39; compact format; info bits 0
*** (2) TRANSACTION:
TRANSACTION 9139671, ACTIVE 2 sec starting index read
mysql tables in use 1, locked 1
6 lock struct(s), heap size 1136, 6 row lock(s), undo log entries 5
MySQL thread id 34874, OS thread handle 140228542277376, query id 65585523 172.17.1.2 user updating
update order set ... where id=*
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 7035 page no 9 n bits 120 index PRIMARY of table `test`.`product` trx id 9139671 lock_mode X locks rec but not gap
Record lock, heap no 50 PHYSICAL RECORD: n_fields 39; compact format; info bits 0
...
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 7002 page no 47 n bits 112 index PRIMARY of table `test`.`order` trx id 9139671 lock_mode X locks rec but not gap waiting
Record lock, heap no 42 PHYSICAL RECORD: n_fields 30; compact format; info bits 0
死锁分析
MySQL死锁日志分析
关于死锁的信息,MySQL 只保留了最后一个死锁的现场,但这个现场还是不完备的。根据MySQL死锁日志,可以得出一下信息:
- 事务1使用主键索引更新product表,等待product表主键上锁模式为写锁的行锁。
- 事务2持有product表中主键索引上模式为写锁的行锁,也就是事务1正在等待的锁。
- 事务2等待order表中主键索引上模式为写锁的行锁。
仅仅根据以上信息并不能得出死锁发生的原因,因为事务2等待的锁被谁持有是未知的,只能结合业务代码来推断谁持有order表中对应记录的锁。
关于MySQL的锁机制以及死锁分析可以学习极客时间《MySQL实战45讲》以及某大佬的博客aneasystone’s blog。
程序日志分析和业务代码分析
看看错误日志中记录死锁发生的代码:orderRepo.findByProductId(productId)
。WTF,find就是普通的select语句啊,又不会加锁,为什么会死锁呢?
联系到Spring Data JPA底层使用的是Hibernate,而Hibernate默认并不是调用Repository的save方法就立即执行更新或插入:Hibernate的持久化上下文会跟踪实体的状态,提交的更新或插入并不会立即flush到数据库,只有在特定的时间才会flush到数据库中。默认的flush模式下情况下,Hibernate会在以下三种情况下将持久化上下文中的“脏”实体flush到数据库:
- 事务提交前。
- 执行和”脏“实体有关的JPQL/HQL查询前。
- 执行任何没有注册同步flush的原生SQL查询前。
有关Hibernate的flush可以参阅Hibernate的Flush机制。
再看看product实体和order实体的关联关系:
@Data
@Table(name = "product")
public class Product{
@Id
private String id;
@Column
private String name;
}
@Table(name = "order")
public class Order{
@Id
private String id;
@JoinColumn(name = "product_id", referencedColumnName = "id", updatable = false, insertable = false)
@ManyToOne
private Procuct product;
}
以及@ManyToOne
的源码:
public @interface ManyToOne {
...
/**
* (Optional) Whether the association should be lazily
* loaded or must be eagerly fetched. The EAGER
* strategy is a requirement on the persistence provider runtime that
* the associated entity must be eagerly fetched. The LAZY
* strategy is a hint to the persistence provider runtime.
*/
FetchType fetch() default EAGER;
...
}
其中Order关联了Product,并且默认是EAGER关联的。也就是说,查询Order的时候,会先查询order表,然后查询product表。有了这些知识准备,再联系业务代码,就可以还原死锁过程了(这里我们假设product表中有一条记录id=p1,order表中有一条记录id=o1,且对应order记录的product_id=p1):
事务2 | 事务1 |
---|---|
调用save更新product(未flush) | 调用save更新product(未flush) |
调用findById查询order :由于关联关系,触发EAGER加载,查询product表;由于flush机制,执行update product where id=p1语句,拿到product主键为p1的记录的X锁 | 调用save更新order表 (未flush) |
调用save更新order表 (未flush) | 调用findById查询order表: 先查询order,由于flush机制,触发执行update order where id=o1,拿到order主键为o1的记录的X锁 |
查询order表:由于flush机制,触发执行update order where id=o1语句,等待order主键为o1的记录的X锁 | 由于关联关系,后查询product表,由于flush机制,触发执行update product where id=p1语句,product主键为p1的记录的X锁 |
简化后如下:
事务2 | 事务1 |
---|---|
持有主键为p1的product记录的X锁 | 持有主键为o1的order记录的X锁 |
等待主键为01的order记录的X锁 | 等待主键为p1的product记录的X锁 |
除了事务1持有的order记录的X锁,其他信息和MySQL死锁日志一致。事务1和事务2互相持有对方需要获取的锁,引起死锁。了解了死锁发生的原因,也就能避免死锁了。通常死锁可以通过两种方式避免:
- 尽量按照相同的顺序加锁。
- 尽量减小持有锁的时间。
对应的业务代码解决方式为:
- 修改关联关系
@ManyToOne(fetch=FetchType.LAZY)
,这样事务会在最后提交的时候flush对应的product和order,更新顺序和获取锁顺序是一致的;或者Repository中的save修改为saveAndFlush,也就是立即将product或order的记录flush到数据库中,代码中更新的先后顺序就是更新顺序和获取锁顺序。 - 减少事务中两个save或者saveAndFlush之间的代码,减少持有锁的时间。
save和saveAndFlash的区别可以参考Difference Between save() and saveAndFlush() in Spring Data JPA>
本文地址:https://blog.csdn.net/qq_32734365/article/details/107406320