Skip to content

Latest commit

 

History

History
113 lines (85 loc) · 6.42 KB

16.md

File metadata and controls

113 lines (85 loc) · 6.42 KB

update-wait-lock-mode-x-vs-update-wait-lock-mode-x-locks-gap-before-rec-insert-intention-holds-lock-mode-x-locks-rec-but-not-gap

死锁特征

  1. update WAITING FOR lock mode X
  2. update WAITING FOR lock_mode X locks gap before rec insert intention, HOLDS lock_mode X locks rec but not gap

死锁日志

------------------------
LATEST DETECTED DEADLOCK
------------------------
2019-03-31 02:50:17 0x7f6d180b7700
*** (1) TRANSACTION:
TRANSACTION 400442, ACTIVE 0 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 5 row lock(s)
MySQL thread id 27, OS thread handle 140106532366080, query id 596977 localhost root Searching rows for update
update t16 set xid = 3, valid = 0 where xid = 3
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index xid_valid of table `dldb`.`t16` trx id 400442 lock_mode X waiting
Record lock, heap no 12 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 4; hex 80000001; asc     ;;
 2: len 4; hex 80000005; asc     ;;

*** (2) TRANSACTION:
TRANSACTION 400441, ACTIVE 0 sec updating or deleting
mysql tables in use 1, locked 1
6 lock struct(s), heap size 1136, 9 row lock(s), undo log entries 2
MySQL thread id 29, OS thread handle 140106531567360, query id 596975 localhost root updating
update t16 set xid = 3, valid = 1 where xid = 2
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 23 page no 4 n bits 80 index xid_valid of table `dldb`.`t16` trx id 400441 lock_mode X locks rec but not gap
Record lock, heap no 12 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 4; hex 80000001; asc     ;;
 2: len 4; hex 80000005; asc     ;;

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 23 page no 4 n bits 80 index xid_valid of table `dldb`.`t16` trx id 400441 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
 0: len 4; hex 80000003; asc     ;;
 1: len 4; hex 80000001; asc     ;;
 2: len 4; hex 80000003; asc     ;;

*** WE ROLL BACK TRANSACTION (1)

表结构

CREATE TABLE `t16` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `xid` int(11) DEFAULT NULL,
  `valid` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `xid_valid` (`xid`,`valid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

初始数据:

INSERT INTO t16(id, xid, valid) VALUES(1, 1, 0);
INSERT INTO t16(id, xid, valid) VALUES(2, 2, 1);
INSERT INTO t16(id, xid, valid) VALUES(3, 3, 1);
INSERT INTO t16(id, xid, valid) VALUES(4, 1, 0);
INSERT INTO t16(id, xid, valid) VALUES(5, 2, 0);
INSERT INTO t16(id, xid, valid) VALUES(6, 3, 1);
INSERT INTO t16(id, xid, valid) VALUES(7, 1, 1);
INSERT INTO t16(id, xid, valid) VALUES(8, 2, 1);
INSERT INTO t16(id, xid, valid) VALUES(9, 3, 0);
INSERT INTO t16(id, xid, valid) VALUES(10, 1, 1);

重现步骤

Session 1 Session 2
update t16 set xid = 3, valid = 0 where xid = 3; update t16 set xid = 3, valid = 1 where xid = 2;

分析

首先要明白 UPDATE 的加锁顺序:在 InnoDB 中,通过二级索引更新记录,首先会在 WHERE 条件使用到的二级索引上加 Next-Key 类型的X锁,以防止查找记录期间的其它插入/删除记录,然后通过二级索引找到 primary key 并在 primary key 上加 Record 类型的X锁,之后更新记录并检查更新字段是否是其它索引中的某列,如果存在这样的索引,通过 update 的旧值到二级索引中删除相应的 entry,此时x锁类型为 Record。

这个死锁和案例 17 的场景一模一样,但是触发死锁的时机稍有不同,可以先看下案例 17 的死锁,这个比较好理解。

和案例 17 一样,对二级索引 xid_valid 的更新过程如下所示:

在事务 2 中, update t16 set xid = 3, valid = 1 where xid = 2 首先会对 xid = 2 的三条记录加上 Next-Key 锁,并在 2-1|83-0|9 之间加上 Gap 锁,然后开始更新二级索引,更新后的位置会加记录锁(lock_mode X locks rec but not gap),而在更新的时候,会将新纪录插入到新的位置,所以和 INSERT 加锁流程类似,需要加插入意向锁,如果该位置有 Gap 锁,则会阻塞。

不过要注意的是,如果同时更新多条记录,MySQL 和 InnoDb 之间的交互如下:

从图中可以看到当 UPDATE 语句被发给 MySQL 后,MySQL Server 会根据 WHERE 条件读取第一条满足条件的记录,然后 InnoDB 引擎会将第一条记录返回并加锁(current read),待 MySQL Server 收到这条加锁的记录之后,会再发起一个 UPDATE 请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,MySQL 在操作多条记录时 InnoDB 与 MySQL Server 的交互是一条一条进行的,加锁也是一条一条依次进行的,先对一条满足条件的记录加锁,返回给 MySQL Server,做一些 DML 操作,然后在读取下一条加锁,直至读取完毕。

所以看死锁日志,就大概能明白死锁的过程了:

事务 2 首先处理第一条满足 WHERE 条件的记录 2-0|5,将 2-0|5 更新成 3-1|5,这个过程会在 2-0|5 上加 Next-Key 锁,并在 3-1|5 上加记录锁,然后以此类推,处理后面的记录。

与此同时,在事务 1 中,update t16 set xid = 3, valid = 0 where xid = 3 会对 xid = 3 的记录挨个处理,特别要注意的是,这里的查询使用的是当前读,所以事务 2 刚刚插入进来的 3-1|5 一样可以读到。所以事务 1 对 3-0|93-1|33-1|53-1|6 挨个处理,首先是 3-0|9 无需处理,然后是 3-1|3 更新成 3-0|3,会在 3-1|3 上加 Next-Key 锁,并在 3-0|3 上加记录锁,然后到处理 3-1|5 的时候,也会对 3-1|5 加 Next-Key 锁,但是这和事务 2 加在 3-1|5 上的记录锁冲突,所以事务 1 阻塞。

而事务 2 在更新完 2-0|5 之后,继续将 2-1|2 更新成 3-1|2,这不仅需要在 2-1|2 上加 Next-Key 锁,在 3-1|2 上加记录锁,而且还要在 3-0|93-1|3 之间加插入意向锁,这和事务 1 加在 3-1|3 上的 Next-Key 锁冲突了,从而导致死锁。

参考

  1. InnoDB inplace-update加锁流程分析 | Learn and live.