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
- update WAITING FOR lock mode X
- 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|8
和 3-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|9
,3-1|3
,3-1|5
和 3-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|9
和 3-1|3
之间加插入意向锁,这和事务 1 加在 3-1|3
上的 Next-Key 锁冲突了,从而导致死锁。