【问题标题】:Nested update with select deadlock带有选择死锁的嵌套更新
【发布时间】:2013-08-19 16:29:27
【问题描述】:

背景

我正在使用一些似乎经常与自身死锁的代码。在 Java 中,它会周期性地产生一个DeadLockLoserDataAccessException,而导致死锁的违规语句通常是本身。 (这是在与 InnoDB 的事务中运行的)

UPDATE a
SET
    a_field = (SELECT sum(b_field) FROM b WHERE b.a_id = a.id)
WHERE 
    a = ?

在做了一些阅读之后,我遇到了执行锁定读取的FOR UPDATE 子句。所以我修改了下面的代码

UPDATE a
SET
    a_field = (SELECT sum(b_field) FROM b WHERE b.a_id = a.id FOR UPDATE)
WHERE 
    a = ?

问题

在嵌套的UPDATE/SELECT 中添加FOR UPDATE 锁是否合适? Locking Reads Documentation 上的示例都没有以这种方式使用FOR UPDATE

表格结构

以下是简化版,其中的字段仅适用于查询

表 A

id      int(11) PRIMARY KEY
a_field int(11)

表 B

id      int(11) PRIMARY KEY
a_id    int(11) FOREIGN KEY REFERENCES (a.id)
b_field int(11)

索引

唯一存在的索引是两个主键上的单列索引,以及表 a 的外键。

【问题讨论】:

  • 是的,您可以在子查询中使用 FOR UPDATE 子句。它将对表 B 中的所有选定记录进行写锁定(但要注意 - 如果 b.a_id 列上没有索引,MySql 可以锁定 B 中的所有记录)。
  • @kordirko 关于表 B 上的索引,该索引是否需要完全覆盖b_field 以及b.id 上的索引?
  • 仅在b_field 上创建索引就足够了。如果该列存在外部约束,MySql 会自动为该列创建索引。但是,我不确定在子查询中放置 FOR UPDATE 是否可以解决这个问题,可能是死锁的来源与某些外键约束有关,您能否编辑您的问题并附加这两个表的结构(使用命令show create table tablename) ?
  • @kordirko 好的,在查看表格及其索引后(使用SHOW INDEX FROM [TBL]),以上是修改后的信息!

标签: java mysql innodb


【解决方案1】:

您的问题的简单答案是:

是的,MySql 在子查询中支持@​​987654321@ 子句

但是这肯定不能解决您的问题。
在这种情况下,子查询中的 FOR UPDATE 不能防止死锁

由于您没有向我们展示整个交易,而只是一个 sn-p,我猜测交易中必须有其他命令对外键引用的记录进行锁定。

为了更好地理解 MySql 中的锁定是如何工作的,看看这个简单的例子:

CREATE TABLE `a` ( 
   `id` int(11) primary key AUTO_INCREMENT, 
   `a_field` int(11) 
);
CREATE TABLE `b` ( 
   `id` int(11) primary key AUTO_INCREMENT, 
   `a_id` int(11), 
   `b_field` int(11),
   CONSTRAINT `b_fk_aid` FOREIGN KEY (`a_id`) REFERENCES `a` (`id`)
);
CREATE TABLE `c` ( 
   `id` int(11) primary key AUTO_INCREMENT, 
   `a_id` int(11), 
   `c_field` int(11),
   CONSTRAINT `c_fk_aid` FOREIGN KEY (`a_id`) REFERENCES `a` (`id`)
);

insert into a( a_field ) values ( 10 ), ( 20 );
insert into b( a_id, b_field ) values ( 1, 20 ), ( 2, 30 );

delimiter $$
create procedure test( p_a_id int, p_count int )
begin
   declare i int;
   set i = 0;
   REPEAT
      START TRANSACTION;
      INSERT INTO c( a_id, c_field ) values ( p_a_id, round(rand() * 100) );
      UPDATE a
         SET  a_field = (
                   SELECT sum(b_field) 
                   FROM b WHERE b.a_id = a.id 
                   FOR UPDATE)
         WHERE 
                id = p_a_id;
       commit; 
       set i = i + 1;
   until i > p_count 
   end repeat;
end $$
DELIMITER ;

注意FOR UPDATE 用在子查询中。
如果我们同时在两个会话中执行该过程:

call test( 2, 400 );

我们几乎同时得到一个死锁错误:

------------------------
LATEST DETECTED DEADLOCK
------------------------
2013-09-05 23:08:27 1b8c
*** (1) TRANSACTION:
TRANSACTION 1388056, ACTIVE 0 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 2, locked 2
LOCK WAIT 5 lock struct(s), heap size 1248, 2 row lock(s), undo log entries 1
MySQL thread id 6, OS thread handle 0x1db0, query id 3107246 localhost 127.0.0.1 test updating
UPDATE a
         SET  a_field = (
                   SELECT sum(b_field) 
                   FROM b WHERE b.a_id = a.id 
                   FOR UPDATE)
         WHERE 
                id = p_a_id
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 222 page no 3 n bits 72 index `PRIMARY` of table `test`.`a` trx id 1388056 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 000000152e16; asc     . ;;
 2: len 7; hex 2d0000013b285a; asc -   ;(Z;;
 3: len 4; hex 8000001e; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 1388057, ACTIVE 0 sec starting index read, thread declared inside InnoDB 5000
mysql tables in use 2, locked 2
5 lock struct(s), heap size 1248, 2 row lock(s), undo log entries 1
MySQL thread id 7, OS thread handle 0x1b8c, query id 3107247 localhost 127.0.0.1 test updating
UPDATE a
         SET  a_field = (
                   SELECT sum(b_field) 
                   FROM b WHERE b.a_id = a.id 
                   FOR UPDATE)
         WHERE 
                id = p_a_id
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 222 page no 3 n bits 72 index `PRIMARY` of table `test`.`a` trx id 1388057 lock mode S locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 000000152e16; asc     . ;;
 2: len 7; hex 2d0000013b285a; asc -   ;(Z;;
 3: len 4; hex 8000001e; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 222 page no 3 n bits 72 index `PRIMARY` of table `test`.`a` trx id 1388057 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 4; hex 80000002; asc     ;;
 1: len 6; hex 000000152e16; asc     . ;;
 2: len 7; hex 2d0000013b285a; asc -   ;(Z;;
 3: len 4; hex 8000001e; asc     ;;

*** WE ROLL BACK TRANSACTION (2)
------------

如您所见,MySql 报告死锁错误是由相同的两个更新引起的。

然而,这只是事实的一半。

死锁错误的真正原因是 INSERT INTO c 语句,它在 A 表中的引用记录上放置了共享锁(因为 C 表中的 FOREIGN KEY 约束)。

而且 - 令人惊讶的是 - 为了防止死锁,必须在事务开始时在A 表中的一行上放置一个锁:

  declare dummy int;
  ...... 
  START TRANSACTION;
      SELECT id INTO dummy FROM A 
      WHERE id = p_a_id FOR UPDATE;
      INSERT INTO c( a_id, c_field ) values ( p_a_id, round(rand() * 100) );
      UPDATE a
         SET  a_field = (
                   SELECT sum(b_field) 
                   FROM b WHERE b.a_id = a.id 
              )
         WHERE 
                id = p_a_id;
       commit; 

进行此更改后,程序运行时不会出现死锁。

因此,您可以尝试在交易开始时添加SELECT ... FROM A ... FOR UPDATE

但如果这不起作用,要获得进一步的帮助来解决这个问题,请:

  • 显示整个事务(事务中涉及的所有命令)
  • 显示事务使用的所有表的结构
  • 显示在插入/更新/删除时触发的触发器,用于修改事务涉及的表

【讨论】:

    【解决方案2】:

    如果单个查询陷入死锁,那一定是 MySQL 的 bug。一个单独的事务决不能以死锁结束。 检查单元测试并进入MySQL bug database

    更新行时,某些 RDBMS 会锁定该行以防止复杂/错误的合并算法。可能是您的代码在许多事务上运行并且它们都有死锁?

    如果错误被证实,你可以拆分你的查询(这看起来很简单):

    SELECT id FROM a WHERE id=? FOR UPDATE;
    SELECT SUM(b_field) FROM b WHERE b.a_id=?;
    UPDATE a SET a_field=? WHERE id=?;
    COMMIT
    

    PS:我想a = ? 的意思是a.id = ?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-17
      • 1970-01-01
      • 2019-05-31
      • 1970-01-01
      • 2014-07-11
      • 2019-05-04
      相关资源
      最近更新 更多