【问题标题】:Avoiding MySQL deadlock when upgrading shared to exclusive lock将共享锁升级为独占锁时避免 MySQL 死锁
【发布时间】:2016-12-07 11:05:48
【问题描述】:

我使用的是 MySQL 5.5。我注意到在并发场景中发生了一个特殊的死锁,我认为这种死锁不应该发生。

像这样重现,使用同时运行的两个 mysql 客户端会话:

mysql 会话 1

create table parent (id int(11) primary key);
insert into parent values (1);
create table child (id int(11) primary key, parent_id int(11), foreign key (parent_id) references parent(id));

begin;
insert into child (id, parent_id) values (10, 1);
-- this will create shared lock on parent(1)

mysql 会话 2

begin;
-- try and get exclusive lock on parent row
select id from parent where id = 1 for update;
-- this will block because of shared lock in session 1

mysql 会话 1

-- try and get exclusive lock on parent row
select id from parent where id = 1 for update;
-- observe that mysql session 2 transaction has been rolled back

mysql 会话 2

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

show engine innodb status报告的信息是这样的:

------------------------
LATEST DETECTED DEADLOCK
------------------------
161207 10:48:56
*** (1) TRANSACTION:
TRANSACTION 107E67, ACTIVE 43 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 376, 1 row lock(s)
MySQL thread id 13074, OS thread handle 0x7f68eccfe700, query id 5530424 localhost root statistics
select id from parent where id = 1 for update
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 3714 n bits 72 index `PRIMARY` of table `foo`.`parent` trx id 107E67 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000107e65; asc     ~e;;
 2: len 7; hex 86000001320110; asc     2  ;;

*** (2) TRANSACTION:
TRANSACTION 107E66, ACTIVE 52 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1248, 2 row lock(s), undo log entries 1
MySQL thread id 12411, OS thread handle 0x7f68ecfac700, query id 5530425 localhost root statistics
select id from parent where id = 1 for update
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 0 page no 3714 n bits 72 index `PRIMARY` of table `foo`.`parent` trx id 107E66 lock mode S locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000107e65; asc     ~e;;
 2: len 7; hex 86000001320110; asc     2  ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 3714 n bits 72 index `PRIMARY` of table `foo`.`parent` trx id 107E66 lock_mode X locks rec but not gap waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000107e65; asc     ~e;;
 2: len 7; hex 86000001320110; asc     2  ;;

*** WE ROLL BACK TRANSACTION (1)

您可以看到事务 (1) 没有显示任何已获得的 S 或 X 锁;它只是被阻止试图获取独占锁。既然没有循环,这种情况应该不会出现死锁,据我了解。

这是一个已知的 MySQL 错误吗?其他人遇到过吗?使用了哪些解决方法?

这些是我们可以采取的可能步骤:

  • 减少外键的使用(在我们的生产场景中,我们只软删除引用表中的行,但是很糟糕)
  • 预先获取排他锁而不是隐式共享锁(会降低我们的并发吞吐量)
  • 更改我们的逻辑,以便我们不再需要在添加子行的同一事务中对父级进行排他锁(风险和困难)
  • 将我们的 MySQL 版本更改为不会出现这种行为的版本

还有其他我们没有考虑的选择吗?

【问题讨论】:

  • 刚刚复制了上面的步骤。 Mysql 5.1.73 UPD 上没有错误。但错误存在于 5.7.17,所以我认为,它是版本 > 5.5 的特定行为
  • bugs.mysql.com/bug.php?id=48652 特别是 Marko Mäkelä 于 2012 年 10 月 22 日 12:32 发表的评论。

标签: mysql sql deadlock


【解决方案1】:

这是一个长期存在的错误,您可以从以下网址了解更多信息:This bug report

这是 MySQL 级别的表锁定问题。

在 InnoDB 内部,FOREIGN KEY 约束检查可以读取(或者, 使用 ON UPDATE 或 ON DELETE 子句,写入)父表或子表。

通常,表访问由以下锁控制: 1. MySQL 元数据锁 2. InnoDB 表锁 3. InnoDB 记录锁

所有这些锁都被持有到事务结束。

在某些模式下会跳过 InnoDB 表和记录锁,但 不是在外键检查期间。死锁是因为 MySQL 只为显式的表获取元数据锁 SQL 语句中提到的。

我想一种解决方法可能是访问孩子(或父母) 事务开始时的表,在有问题的 FOREIGN 之前 按键操作。

阅读讨论和它的回复

【讨论】:

    【解决方案2】:

    没有给出更新父行的原因, 但我认为这与一些去规范化有关,基于问题中的这个序列:

    -- session 1
    begin;
    insert into child (id, parent_id) values (10, 1);
    ...
    select id from parent where id = 1 for update;
    

    例如,一个订单(父表)有一列金额, 它被维护为所有订单行的金额的总和(子 表)。

    似乎维护父数据的逻辑是在应用程序中编码的 本身(带有显式更新语句),具有以下后果:

    • 如果在许多不同的地方插入子元素, 那么客户端中的应用程序逻辑必须在所有这些地方更新 保持完整性。这是代码重复。

    • 即使这仅在一个地方完成,父表的事实 当服务器无法找到添加孩子时,需要更新。

    请考虑以下选项:

    在子表上定义触发器,根据需要更新父表。

    它具有以下含义:

    • 首先,维护父表的逻辑不再(可能) 重复,因为它在触发器本身中。

    • 第二,这是这里的重要部分,MySQL 服务器现在知道 每当插入子记录时,父表就会更新,并且因为 其中,会采用适当的锁(独占而不是共享)。

    用 8.0 测试,见下文。

    关于并发吞吐量的问题,

    • 在不同父行上操作的不同事务将在 并行,因为排他锁是在父(不同)行上进行的,而不是 父表。

    • 在同一父行上同时运行的事务确实会 被序列化......这实际上是预期的结果,因为它们完成 还是一样的记录。

    保证成功的序列化事务应该提供更好的吞吐量(就应用程序工作负载而言), 某些事务失败,只能重试。

    显然,还需要更新和删除触发器,以更新父级,具体取决于应用程序逻辑。

    设置

    create table parent (
      id int(11) primary key,
      number_of_children int(11));
    
    create table child (
      id int(11) primary key,
      parent_id int(11),
      foreign key (parent_id) references parent(id));
    
    delimiter $$;
    create trigger bi_child before insert on child
    for each row
    begin
      update parent
        set number_of_children = number_of_children + 1
        where id = NEW.parent_id;
    end
    $$
    delimiter ;$$
    
    begin;
    insert into parent values (1, 0);
    insert into parent values (2, 0);
    commit;
    

    会话 1

    begin;
    insert into child values (10, 1);
    

    第 2 场

    begin;
    insert into child values (20, 2);
    

    未阻止,因为使用了不同的父级。

    第 3 节

    begin;
    -- this now blocks, waiting for an X lock on parent row 1.
    insert into child values (11, 1);
    

    会话 1

    -- unlocks session 3
    commit;
    

    第 3 节

    提交;

    第 2 场

    提交;

    结果

    select * from parent;
    id      number_of_children
    1       2
    2       1
    

    【讨论】:

    • 不,不是反规范化,逻辑不能作为触发器实现。父级的状态本质上表明后台处理正在发生。异步地,将行添加到引用父行的另一个表中。但是如果父行已经处于状态,我们不会启动异步作业。所以我们在父行上使用排他锁来控制与后台作业的同步。奇怪的是,应该是安全的序列变成了检测到的死锁。
    • @BarryKelly,很抱歉听到触发器不适用于您的情况。这个约束也没有记录在问题中......如果你问“我们没有考虑其他选项吗?”,我认为解释你想要实现的目标是公平的。
    • 我知道。对不起。我将其缩减为一组最小的可重现步骤。在这样做的过程中,我超越了我的应用程序要求。但是,如果我描述了我的应用需求,我们就会进行更大范围的讨论。
    猜你喜欢
    • 2014-08-11
    • 1970-01-01
    • 1970-01-01
    • 2015-04-16
    • 1970-01-01
    • 2013-07-24
    • 2012-08-14
    • 2012-08-03
    • 1970-01-01
    相关资源
    最近更新 更多