今天来分享一下我在线上环境遇到的有关MySQL间隙锁的死锁问题。本文将讲述从发现问题到解决问题的全过程,并给出一些个人建议,其中使用的数据将做脱敏处理,但不影响食用口感。
运维同学发现有大量的MySQL死锁日志输出,如果你的系统有对MySQL进行死锁监控,可会在图形界面上很直观的发现这个问题。
你会发现日志里有大量下面这样的log输出:
Deadlock found when trying to get lock; try restarting transaction
既然知道了是死锁造成的问题,那怎么定位问题呢?我们可以使用show engine innodb status
查看最近死锁发生的日志,下面只给出与本次死锁有关的信息:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-05-10 05:54:26 0x7f08bd92e700
*** (1) TRANSACTION:
TRANSACTION 222522, ACTIVE 84 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 5 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 9, OS thread handle 139675518330624, query id 126 192.168.0.1 root updating
update t set a = 1 where b= 4
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 19 page no 6 n bits 80 index b of table `mydb`.`t` trx id 222522 lock_mode X waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 4; hex 80000004; asc ;;
*** (2) TRANSACTION:
TRANSACTION 222523, ACTIVE 81 sec starting index read
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 10, OS thread handle 139675516987136, query id 127 192.168.0.1 root updating
update t set b = 1 where a= 3
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 19 page no 6 n bits 80 index b of table `mydb`.`t` trx id 222523 lock_mode X
Record lock, heap no 5 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 4; hex 80000004; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 19 page no 5 n bits 1136 index a of table `mydb`.`t` trx id 222523 lock_mode X waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 4; hex 80000003; asc ;;
*** WE ROLL BACK TRANSACTION (2)
从上面日志可以清楚的看到,TRANSACTION(1)正在等待RECORD LOCKS space id 19 page no 6 n bits 80 index b of table mydb.t
,而这个锁正好被TRANSACTION(2)持有。而TRANSACTION(2)正在等待的锁RECORD LOCKS space id 19 page no 5 n bits 1136 index a of table mydb.t
正好是TRANSACTION(1)持有的锁,这样就导致了死锁。
既然已经遭到了问题所在,接下来我们来看看是如何产生这个死锁的。
在这之前,需要准备一下结构和数据:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`a` int(11) NULL DEFAULT NULL,
`b` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `a`(`a`) USING BTREE,
INDEX `b`(`b`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
INSERT INTO `t` VALUES (1, 1, 1);
INSERT INTO `t` VALUES (2, 2, 2);
INSERT INTO `t` VALUES (3, 3, 3);
INSERT INTO `t` VALUES (4, 4, 4);
INSERT INTO `t` VALUES (5, 5, 5);
为了方面说明和数据脱敏,这里对表结构和数据做了相应处理,但丝毫不影响我们要说明的问题。现在有两个session同时访问数据库,他们访问的次序如下表:
时间 | session1 | session2 |
---|---|---|
T1 | start transaction; select * from t where a=3 for update; /* Lock a = (2,4] */ |
|
T2 | start transaction; select * from t where b = 4 for update; /* Lock b = (3,5] */ |
|
T3 | update t set a = 1 where b= 4; /* waiting session2 unlock b*/ |
|
T4 | update t set b = 1 where a= 3; /* waiting session1 unlock a, ROLL BACK*/ |
|
T5 | /* Query OK, 1 row affected */ commit; |
现在我们来分析一下整个过程:
WE ROLL BACK TRANSACTION (2)
;上面我们提到的间隙锁是怎么回事呢?InnoDB不是用行锁么,为什么还有一个间隙锁呢?其实间隙锁的出现是解决幻读的问题的。这里通过下面的例子简单介绍一下何为幻读。
在没有间隙锁的情况下,只使用行锁,看看会怎么样:
时间 | session1 | session2 |
---|---|---|
T1 | start transaction; select * from t where a=3 for update; result:(3,3,3) |
|
T2 | update t set a = 3 where id = 1; | |
T3 | start transaction; select * from t where a=3 for update; result:(1,3,1),(3,3,3) |
|
T4 | insert into t values(6,6,3); | |
T5 | select * from t where a=3 for update; result:(1,3,1),(3,3,3),(6,6,3) |
|
T6 | commit; |
其实上面这个表格已经十分清楚了,session1在不同的时间使用了读锁定,3次的结果都不一样,而在T5时刻读到的(6,6,3)被认为是幻读。那为什么T3时刻都到的(1,3,1)不是幻读呢?
其实幻读是针对新增或者删除所产生的读结果,对于修改所产生的读结果别认为是“当前读”。
由于行锁无法解决幻读问题,所以InnoDB引入了间隙锁来解决幻读。
那何为间隙锁呢,其实顾名思义,就是锁定间隙的锁,MySQL定义了间隙锁的锁范围为前开后闭区间。下面举个例子说明这个问题:
INSERT INTO `t` VALUES (10, 10, 10);
INSERT INTO `t` VALUES (20, 20, 20);
INSERT INTO `t` VALUES (30, 30, 30);
往表 t 里插入上面几条数据,在每个列上就会形成4个间隙锁区间,分别为(-∞,10],(10,20],(20,30],(30,+∞]。
你可能会觉得奇怪,InnoDB又是怎么锁定间隙的呢?我们都知道,行锁其实是通过锁定索引来达到加锁的目的的,到这里,你应该马上能想到,间隙锁其实也可以这样做,每个间隙其实是连续的有序区间,InnoDB的B+Tree不就是有序的么,通过锁定一个非叶子节点就能锁住一个区间,防止往这个区间添加或删除数据。
那没有索引的列怎么办,行锁还可以通过主键或者row_id进行锁定,可是范围区间却不能。如果需要使用到间隙锁,而对应列上没有索引的话,InnoDB会锁表,没锁,就是锁表。这一点需要注意。如果你在一个事务里执行一个update by where,字句中的列没有索引的话就会锁表。
又此可见,虽然InnoDB引入了间隙锁解决幻读问题,但同时也带来了死锁,性能等问题。
其实MySQL已经引入了死锁解决策略,通过上面提到的死锁策略中的最后一行WE ROLL BACK TRANSACTION (2)
我们就可以发现。那我我们需要做的和可以做的就是让MySQL运行时尽量避免死锁:
今天我给出了一个线上问题的处理过程,其中分析了MySQL的幻读和解决幻读的所适用的手段——间隙锁,并分析了间隙锁带来的一些问题等等。