jpa hibernate死锁与简单的find+update



当两个事务正在进行时,我经常出现死锁:

  • entitymanager.find by id,无锁定模式
  • entitymanager.merge,没有特定的锁定模式

它们都在@Transactional下,默认的隔离是可重复读取,在mysql5.7下。通常使用的id为PK的实体自动递增。没有@版本,如果这很重要的话。。。

结果是:

  1. txn A查找行
  2. txn B查找行
  3. txn A尝试更新并因此升级到独占X锁,但等待,因为在txn B的行上似乎存在共享(S)(读取)锁
  4. txn B试图更新并因此升级到独占X锁,但它在txn A之后,后者被B自身阻止。现在,这被检测为死锁,因此其中一个txn将回滚

SHOW ENGINE INNODB STATUS(SEIS)显示上次检测到的死锁。它清楚地表明有共享的(S)锁。

这是我从prod的SEIS(为了隐私起见,重新标记了一些)。

*** (1) TRANSACTION:
TRANSACTION 175274419, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 8 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 2
MySQL thread id 627088, OS thread handle 22952098592512, query id 365172765 192.168.1.100 mydatabase updating
update thing set needs_checkup=0 where id=1129
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 361 page no 25 n bits 144 index PRIMARY of table `mydatabase`.`thing` trx id 175274419 lock_mode X locks rec but not gap waiting

*** (2) TRANSACTION:
TRANSACTION 175274420, ACTIVE 0 sec starting index read
mysql tables in use 1, locked 1
8 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 2
MySQL thread id 627077, OS thread handle 22952240928512, query id 365172766 192.168.1.100 mydatabase updating
update thing set needs_checkup=0 where id=1129
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 361 page no 25 n bits 144 index PRIMARY of table `mydatabase`.`thing` trx id 175274420 lock mode S locks rec but not gap

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 361 page no 25 n bits 144 index PRIMARY of table `mydatabase`.`thing` trx id 175274420 lock_mode X locks rec but not gap waiting
*** WE ROLL BACK TRANSACTION (2)

令人惊讶的是:我在org.hubinate.SQL上启用了hibernate DEBUG级别来查看这些语句,其中没有一个显示任何";选择锁定在共享模式中";(也不选择"…"进行更新)。

(我已经做了额外的努力,通过端口3306用wireshark对mysql协议进行了数据包嗅探,没有任何特殊锁定模式的提示,也没有任何会话变量,除了经常出现的"设置会话事务读写"与"…只读"之外,这对锁定没有影响)。

显然,在步骤1和步骤3之间有足够的时间让txn B偷偷进入。所以我认为这个共享锁不是update语句的瞬时影响。如果是这样的话,我们就不会那么容易陷入僵局。因此,我推测共享锁来自";查找";。

问题是这是在哪里配置的?对于我阅读的所有文档,默认的锁定模式是LockMode.NONE.

如果我在两个会话中编写原始sql,如下所示(默认情况下使用事务读写模式),我不会出现死锁:

  1. txnA:从foo中选择*,其中id=1
  2. txnB:从foo中选择*,其中id=1
  3. txnA:更新foo集合x=x+1,其中id=1
  4. txnB:更新foo集合x=x+1000,其中id=1

但如果我写这个,那么我会得到相同的死锁:

  1. txnA:在共享模式下,从foo中选择*,其中id=1锁定
  2. txnB:在共享模式下,从foo中选择*,其中id=1锁定
  3. txnA:更新foo集合x=x+1,其中id=1
  4. txnB:更新foo集合x=x+1000,其中id=1

现在,我不想在find中使用X(或U)锁,如"如何使用UPDATE锁防止常见形式的死锁?"中所述?。

我只想减少锁定,因为原始SQL似乎允许这样做。因此,问题是这是在哪里配置的?为什么请求此共享锁?如果我在嗅探的数据包中看到的sql语句都没有提示那些共享锁,hibernate是如何做到这一点的

谢谢。

更新:

在过去的几天里,我研究了在上述更新之前,在相同的交易中出现看不见的语句的可能性。

a) 我确实插入了某个子表行的外键,并指向"thing"行。mysql-doc确实谈到了FK的父级上的共享锁。我试过了,但它没有为共享锁定父级。我仍然无法用那些带有";生的";再次说明。子插入不会阻止父更新(记住,父"thing"表是具有死锁语句的表)。

b) 我也阅读独特的钥匙。他们提到了一些关于语句(不满足唯一约束)使用共享锁的内容。我不清楚实现这一目标的步骤。当我还在调查这件事的时候,我想我应该提到它,以防它引起别人的注意。。。

令人惊讶的是:我在org.hubinate.SQL上启用了hibernate DEBUG级别来查看这些语句,但它们中没有一个显示任何"选择锁定在共享模式中";(也不选择"…"进行更新)。(…)问题是这是在哪里配置的?对于我阅读的所有文档,默认的锁定模式是LockMode.NONE.

默认的锁定行为当然是在RDBMS中配置的。它也被称为隔离级别。您说您的设置为REPEATABLE READ,所以读锁和写锁都会一直保持到事务结束。

除非有明确的请求,否则您似乎对期望不使用锁感到困惑。这根本不是它的工作方式。SELECT语句always获取读锁,UPDATE语句always取写锁。百万美元的问题是,这些锁何时被释放,这就是隔离级别所控制的。

我只想锁定更少的

如果您的用例不关心不可重复的读取,那么切换到更宽松的隔离级别READ COMMITED

或者,如果数据库行之间几乎没有争用,并且更新操作相对便宜,请使用乐观锁定,就像@roccobaroccoSC建议的那样。

或者,如果争用动态变化,请尝试混合方法:首先,尝试n次乐观锁定,如果失败,则使用悲观锁定,并提前调用em.find(..., LockMode.PESSIMISTIC)

您对非锁定读取的期望是正确的,文档中也明确指出了这一点:https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html,然而,我确实认为您对情况的分析是不完整的-当您执行跟踪时,您可能会忽略一些看起来与您无关的语句,但从InnoDB的角度来看,这些语句可能是有意义的,例如,考虑以下DB结构:

mysql> create table t(id int(11), v int(11));
Query OK, 0 rows affected, 2 warnings (0.02 sec)
mysql> insert into t(id,v) primary key, values(1,1),(2,2);
Query OK, 2 rows affected (0.00 sec)
mysql> select * from t;
+------+------+
| id   | v    |
+------+------+
|    1 |    1 |
|    2 |    2 |
+------+------+
2 rows in set (0.00 sec)

并且下一个语句实际上由于"锁定"而锁定了整个表;缺少";v列上的索引:

mysql> update t set v=4 where id<2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
mysql> show engine innodb status;
...
---TRANSACTION 3672, ACTIVE 2 sec
3 lock struct(s), heap size 1128, 2 row lock(s), undo log entries 1

这对我们来说是违反直觉的,但在文件中有描述:https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html:

更新。。。在哪里。。。为每条记录设置一个独占的下一个密钥锁定搜索遇到。但是,只需要一个索引记录锁对于使用唯一索引锁定行以搜索唯一的行。

这正是我遇到的问题:选择和更新导致死锁。

我使用SQL server 2019,其默认隔离级别为Read committed。锁定程度较低的隔离水平是多少?读取未提交?

https://www.sqlservercentral.com/articles/isolation-levels-in-sql-server

最新更新