2016-12-07 93 views
21

我正在使用MySQL 5.5。我注意到在并发场景中发生了一个特殊的死锁,我认为这种僵局不会发生。当升级共享到独占锁时避免MySQL死锁

重现这样,利用同时运行两个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到o ne没有表现出这种行为

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

+1

https://bugs.mysql.com/bug.php?id=48652特别是在2012年10月22日12:32由马尔科Mäkelä评论。 – bishop

+0

刚刚转载了上述步骤。 Mysql 5.1.73 UPD没有错误。 但是,错误存在于5.7.17,所以我认为它是版本> 5.5的具体行为 –

回答

5

这是一个长期存在的BUG,你可以阅读从更多:This bug report

这是在MySQL级表锁定问题。

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

通常,表访问由以下锁管理: 1. MySQL元数据锁 2. InnoDB表锁 3。InnoDB的记录锁

所有这些锁一直持有到交易结束。

的InnoDB表和记录锁定会跳过某些模式,但不 期间外键检查。因为MySQL 获取元数据只对那些明确在SQL语句中提到 表锁(S)的僵局造成的。

我想一个解决方法可能是在问题FOREIGN KEY操作之前,在事务开始时访问子表(或父表) 表。

阅读的讨论,它的答复的

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); 

未被阻塞,作为differen t父母被使用。

会话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 
+0

不,它不是非规范化,并且逻辑不能作为触发器实现。父母的状态基本上表明正在发生背景处理。异步地,行被添加到引用父行的另一个表中。但是,如果父行已经处于该状态,我们不会启动异步作业。所以我们在父行上使用独占锁定来控制与后台作业的同步。奇怪的是,应该是安全的序列变成检测到的僵局。 –

+0

@BarryKelly,很抱歉听到触发器不能用于你的情况。这个约束在这个问题中没有记录......如果你问“我们没有考虑其他选择吗?”,我认为解释你想达到的目标是公平的。 –

+0

我知道。对不起。我把它缩减到了一小部分可重复的步骤。这样做,我超出了我的应用要求。但是如果我描述了我的应用程序需求,我们会进行更大范围的讨论。 –