将共享锁升级为独占锁时避免 MySQL 死锁

Posted

技术标签:

【中文标题】将共享锁升级为独占锁时避免 MySQL 死锁【英文标题】:Avoiding MySQL deadlock when upgrading shared to exclusive lock 【发布时间】: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 发表的评论。 【参考方案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,很抱歉听到触发器不适用于您的情况。这个约束也没有记录在问题中......如果你问“我们没有考虑其他选项吗?”,我认为解释你想要实现的目标是公平的。 我知道。对不起。我将其缩减为一组最小的可重现步骤。在这样做的过程中,我超越了我的应用程序要求。但是,如果我描述了我的应用需求,我们就会进行更大范围的讨论。

以上是关于将共享锁升级为独占锁时避免 MySQL 死锁的主要内容,如果未能解决你的问题,请参考以下文章

使用乐观锁时会不会出现死锁?

mysql死锁

Java锁机制:乐观锁 悲观锁 自旋锁 可重入锁 读写锁 公平锁 非公平锁 共享锁 独占锁 重量级锁 轻量级锁 偏向锁 分段锁 互斥锁 同步锁 死锁 锁粗化 锁消除

Mysql 锁机制和事务

白话Mysql的锁和事务隔离级别!死锁间隙锁你都知道吗?

当多个对象持有一个共享锁并且一个想要升级到独占时,sql中基于锁的并发控制如何工作?