前几天跟一位朋友分析了一个死锁问题,所以有了这篇图文详细的博文,哈哈~
发生死锁了,如何排查和解决呢?本文将跟你一起探讨这个问题
数据库隔离级别:
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
自动提交关闭:
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 0 |
+--------------+
1 row in set (0.00 sec)
表结构:
//id是自增主键,name是非唯一索引,balance普通字段
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
开启两个终端模拟事务并发情况,执行顺序以及实验现象如下:
1)事务A执行更新操作,更新成功
mysql> update account set balance =1000 where name ='Wei';
Query OK, 1 row affected (0.01 sec)
2)事务B执行更新操作,更新成功
mysql> update account set balance =1000 where name ='Eason';
Query OK, 1 row affected (0.01 sec)
3)事务A执行插入操作,陷入阻塞~
mysql> insert into account values(null,'Jay',100);
这时候可以用select * from information_schema.innodb_locks;
查看锁情况:
4)事务B执行插入操作,插入成功,同时事务A的插入由阻塞变为死锁error。
mysql> insert into account values(null,'Yan',100);
Query OK, 1 row affected (0.01 sec)
在分析死锁日志前,先做一下锁介绍,哈哈~
InnoDB 实现了标准的行级锁,包括两种:共享锁(简称 s 锁)、排它锁(简称 x 锁)。
如果事务 T1 持有行 r 的 s 锁,那么另一个事务 T2 请求 r 的锁时,会做如下处理:
如果 T1 持有 r 的 x 锁,那么 T2 请求 r 的 x、s 锁都不能被立即允许,T2 必须等待T1释放 x 锁才可以,因为X锁与任何的锁都不兼容。
比如:事务1在表1上加了S锁后,事务2想要更改某行记录,需要添加IX锁,由于不兼容,所以需要等待S锁释放;如果事务1在表1上加了IS锁,事务2添加的IX锁与IS锁兼容,就可以操作,这就实现了更细粒度的加锁。
InnoDB存储引擎中锁的兼容性如下表:
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE
记录锁的事务数据(关键词:lock_mode X locks rec but not gap
),记录如下:
RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10078 lock_mode X locks rec but not gap
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 8000000a; asc ;;
1: len 6; hex 00000000274f; asc 'O;;
2: len 7; hex b60000019d0110; asc ;;
间隙锁的事务数据(关键词:gap before rec
),记录如下:
RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table `test2`.`account`
trx id 38049 lock_mode X locks gap before rec
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 3; hex 576569; asc Wei;;
1: len 4; hex 80000002; asc ;;
事务数据类似于下面:
RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 4; hex 80000066; asc f;;
1: len 6; hex 000000002215; asc " ;;
2: len 7; hex 9000000172011c; asc r ;;...
锁模式兼容矩阵(横向是已持有锁,纵向是正在请求的锁):
可以用show engine innodb status
,查看最近一次死锁日志哈~,执行后,死锁日志如下:
2020-04-11 00:35:55 0x243c
*** (1) TRANSACTION:
TRANSACTION 38048, ACTIVE 92 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
MySQL thread id 53, OS thread handle 2300, query id 2362 localhost ::1 root update
insert into account values(null,'Jay',100)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table `test2`.`account`
trx id 38048 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 3; hex 576569; asc Wei;;
1: len 4; hex 80000002; asc ;;
*** (2) TRANSACTION:
TRANSACTION 38049, ACTIVE 72 sec inserting, thread declared inside InnoDB 5000
mysql tables in use 1, locked 1
5 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 2
MySQL thread id 52, OS thread handle 9276, query id 2363 localhost ::1 root update
insert into account values(null,'Yan',100)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table `test2`.`account`
trx id 38049 lock_mode X locks gap before rec
Record lock, heap no 6 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 3; hex 576569; asc Wei;;
1: len 4; hex 80000002; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 177 page no 4 n bits 80 index idx_name of table `test2`.`account`
trx id 38049 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (1)
我们如何分析以上死锁日志呢?
1)找到关键词TRANSACTION,事务38048
2)查看正在执行的SQL
insert into account values(null,'Jay',100)
3)正在等待锁释放(WAITING FOR THIS LOCK TO BE GRANTED),插入意向排他锁(lock_mode X locks gap before rec insert intention waiting),普通索引(idx_name),物理记录(PHYSICAL RECORD),间隙区间(未知,Wei);
1)找到关键词TRANSACTION,事务38049
insert into account values(null,'Yan',100)
3)持有锁(HOLDS THE LOCK),间隙锁(lock_mode X locks gap before rec),普通索引(index idx_name),物理记录(physical record),区间(未知,Wei);
4)正在等待锁释放(waiting for this lock to be granted),插入意向锁(lock_mode X insert intention waiting),普通索引上(index idx_name),物理记录(physical record),间隙区间(未知,+∞);
5)事务1回滚(we roll back transaction 1);
这里面,有些朋友可能有疑惑,
我们接下来一小节详细分析一波,一个一个问题来~
为了方便记录,例子用W表示Wei,J表示Jay,E表示Eason哈~
update account set balance =1000 where name ='Wei';
间隙锁:
记录锁
Next-Key锁
综上所述,事务A执行完update更新语句,会持有锁:
insert into account values(null,'Jay',100);
间隙锁:
插入意向锁(Insert Intention)
因此,事务A的update语句和insert语句执行完,它是持有了 (E,W]的 Next-Key锁,(W,+∞)的Gap锁,想拿到 (E,W)的插入意向排它锁,等待的锁跟死锁日志是对上的,哈哈~
update account set balance =1000 where name ='Eason';
间隙锁:
记录锁
Next-Key锁
综上所述,事务B执行完update更新语句,会持有锁:
insert into account values(null,'Yan',100);
间隙锁:
插入意向锁(Insert Intention)
所以,事务B的update语句和insert语句执行完,它是持有了 (-∞,E]的 Next-Key锁,(E,W)的Gap锁,想拿到 (W,+∞)的间隙锁,即插入意向排它锁,加锁情况跟死锁日志也是对上的~
最后,遇到死锁问题,我们应该怎么分析呢?