MySQL死锁的原因有很多,总的来说,还是因为加锁的顺序不一致,下面来看一个index_merge导致的死锁。
死锁案例:
- MySQL版本:5.7.19
- 隔离级别:提交读(RC)
先看表结构,如下:
CREATE TABLE `t` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `c1` varchar(50) DEFAULT NULL, `c2` varchar(50) DEFAULT NULL, `ts` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_c1` (`c1`), KEY `idx_c2` (`c2`), KEY `idx_ts` (`ts`) ) ENGINE=InnoDB AUTO_INCREMENT=3279 DEFAULT CHARSET=utf8mb4
主键id,字段c1,c2,ts 都建有二级索引。
看两个SQL:
update t set ts=now() where c1=’100′ and c2=’100′;
update t set ts=now() where c1=’100′ and c2=’200′;
这两个SQL在多个并发同时执行的情况下,很高的概率会发生死锁。死锁信息如下:
------------------------ LATEST DETECTED DEADLOCK ------------------------ 2020-01-18 11:26:16 0x7f375216a700 *** (1) TRANSACTION: TRANSACTION 24049054, ACTIVE 0 sec starting index read mysql tables in use 3, locked 3 LOCK WAIT 5 lock struct(s), heap size 1136, 4 row lock(s) MySQL thread id 1021, OS thread handle 139876759136000, query id 6147815 10.49.2.92 sndsadmin updating update db.t set ts=now() where c1='100' and c2='200' *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 29 n bits 104 index PRIMARY of table `db`.`t` trx id 24049054 lock_mode X locks rec but not gap waiting *** (2) TRANSACTION: TRANSACTION 24049053, ACTIVE 0 sec starting index read, thread declared inside InnoDB 4999 mysql tables in use 3, locked 3 4 lock struct(s), heap size 1136, 3 row lock(s) MySQL thread id 244, OS thread handle 139875577145088, query id 6147814 10.49.2.92 sndsadmin updating update db.t set ts=now() where c1='100' and c2='100' *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 23 page no 29 n bits 104 index PRIMARY of table `db`.`t` trx id 24049053 lock_mode X locks rec but not gap *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 23 page no 12 n bits 952 index idx_c1 of table `db`.`t` trx id 24049053 lock_mode X locks rec but not gap waiting *** WE ROLL BACK TRANSACTION (2)
看下表中数据分布,表t中一共有2181条记录,c1值为100的有4条记录,c2值为100的有2条记录,c2值为200的有3条记录。如下:
mysql> select count(*) from t; +----------+ | count(*) | +----------+ | 2181 | +----------+ mysql> select * from t where c1='100'; +------+------+------+---------------------+ | id | c1 | c2 | ts | +------+------+------+---------------------+ | 3274 | 100 | 100 | 2020-01-18 11:26:16 | | 3275 | 100 | 200 | 2020-01-18 11:29:53 | | 3277 | 100 | 200 | 2020-01-18 11:29:53 | | 3278 | 100 | 100 | 2020-01-18 11:26:16 | +------+------+------+---------------------+ 4 rows in set (0.00 sec) mysql> select * from t where c2='100'; +------+------+------+---------------------+ | id | c1 | c2 | ts | +------+------+------+---------------------+ | 3274 | 100 | 100 | 2020-01-18 11:26:16 | | 3278 | 100 | 100 | 2020-01-18 11:26:16 | +------+------+------+---------------------+ 2 rows in set (0.00 sec) mysql> select * from t where c2='200'; +------+------+------+---------------------+ | id | c1 | c2 | ts | +------+------+------+---------------------+ | 3275 | 100 | 200 | 2020-01-18 11:30:07 | | 3276 | 200 | 200 | NULL | | 3277 | 100 | 200 | 2020-01-18 11:30:07 | +------+------+------+---------------------+ 3 rows in set (0.00 sec)
c1,c2字段上分别建有非唯一的二级索引,c1,c2有值相同的情况。查看上述两个SQL的执行计划,发现都使用了index_merge,在Extra列使用了Using intersect(idx_c2,idx_c1);
explain update t set ts=now() where c1=’100′ and c2=’100′;
explain update t set ts=now() where c1=’100′ and c2=’200′;
执行计划如下:
mysql> explain update t set ts=now() where c1='100' and c2='100'; +----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+ | 1 | UPDATE | t | NULL | index_merge | idx_c1,idx_c2 | idx_c2,idx_c1 | 203,203 | NULL | 1 | 100.00 | Using intersect(idx_c2,idx_c1); Using where | +----+-------------+-------+------------+-------------+---------------+---------------+---------+------+------+----------+---------------------------------------------+
死锁分析:
本次死锁案例主要由index_merge导致,由于表中索引不合理,MySQL优化器选择了索引合并,即根据where条件分别走idx_c1, idx_c2两个索引,再将结果进行合并。update操作,走了两个二级索引,根据加锁顺序,除了对二级索引加锁外,还要对主键索引进行加锁,多并发执行条件下,获取二级索引的行锁与获取主键索引的行锁不再有顺,最终导致死锁发生。
解决方案:
- 增加联合索引 idx_c1_c2(c1,c2),优化器直接使用联合索引,不再使用index_merge,加锁顺序一致,不再出现死锁。
- SQL语句中强制走某个索引,比如 idx_c1索引的区分度已经非常好了,那么使用 force index(idx_c1),让优化器强制走idx_c1,也不再会出现死锁。
- 关掉参数index_merge_intersection=off,禁用index_merge功能。