出现场景
年前,线上数据库报了一个死锁问题。 首先,表结构:
CREATE TABLE `miza_log`.`blind_date_user_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`blind_date_id` int(11) NOT NULL DEFAULT '0' COMMENT '本轮相亲id',
`user_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户id',
`choose_id` int(11) NOT NULL DEFAULT '0' COMMENT '用户选择的心仪对象id',
`recv_gift` int(11) NOT NULL DEFAULT '0' COMMENT '用户本轮收礼总额',
`send_gift` int(11) NOT NULL DEFAULT '0' COMMENT '用户本轮送礼总额',
`hat_num` int(11) NOT NULL DEFAULT '0' COMMENT '带帽次数',
`img_url` varchar(256) NOT NULL DEFAULT '0' COMMENT '用户结果图片',
`match_result` int(11) NOT NULL DEFAULT '0' COMMENT '0: 配对不成功 1: 配对成功 2: 配对成功且已经发送小秘书',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`),
KEY `blind_date_id` (`blind_date_id`),
KEY `user_id` (`user_id`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户参与相亲信息';
id, blind_date_id 和 user_id 分别建了索引。
发生死锁的事务sql:
update blind_date_user_info set hat_num = hat_num + 1 where blind_date_id = 344 and user_id = 2102142;
update blind_date_user_info set recv_gift = recv_gift + 0 where blind_date_id = 344 and user_id = 99264271;
sql的执行计划:
死锁日志:
=====================================
2021-02-01 16:32:45 0x7f385337f700 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 2 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 1101094 srv_active, 0 srv_shutdown, 40 srv_idle
srv_master_thread log flush and writes: 1101061
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 566444
OS WAIT ARRAY INFO: signal count 2252846
RW-shared spins 0, rounds 2205774, OS waits 248328
RW-excl spins 0, rounds 36978240, OS waits 84670
RW-sx spins 1320303, rounds 11237389, OS waits 54800
Spin rounds per wait: 2205774.00 RW-shared, 36978240.00 RW-excl, 8.51 RW-sx
------------------------
LATEST DETECTED DEADLOCK
------------------------
2021-02-01 16:30:23 0x7f376cf43700
*** (1) TRANSACTION:
TRANSACTION 1928467447, ACTIVE 0 sec fetching rows
mysql tables in use 3, locked 3
LOCK WAIT 10 lock struct(s), heap size 1136, 4 row lock(s)
MySQL thread id 481042, OS thread handle 139877068830464, query id 1006475452 172.17.0.83 root updating
update blind_date_user_info set hat_num = hat_num + 1 where blind_date_id = 344 and user_id = 2102142
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 12499 page no 66 n bits 280 index PRIMARY of table `miza_log`.`blind_date_user_info` trx id 1928467447 lock_mode X locks rec but not gap waiting
Record lock, heap no 186 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
0: len 4; hex 800020fc; asc ;;
1: len 6; hex 000072f213f6; asc r ;;
2: len 7; hex 770000707a116f; asc w pz o;;
3: len 4; hex 80000158; asc X;;
4: len 4; hex 85eaa70f; asc ;;
5: len 4; hex 80000000; asc ;;
6: len 4; hex 80001e46; asc F;;
7: len 4; hex 800003e8; asc ;;
8: len 4; hex 8000001f; asc ;;
9: len 0; hex ; asc ;;
10: len 4; hex 80000000; asc ;;
11: len 4; hex 6017b6b9; asc ` ;;
12: len 4; hex 6017bc1f; asc ` ;;
*** (2) TRANSACTION:
TRANSACTION 1928467448, ACTIVE 0 sec fetching rows
mysql tables in use 3, locked 3
7 lock struct(s), heap size 1136, 3 row lock(s)
MySQL thread id 546874, OS thread handle 139876027873024, query id 1006475453 172.17.0.2 root updating
update blind_date_user_info set recv_gift = recv_gift + 0 where blind_date_id = 344 and user_id = 99264271
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 12499 page no 66 n bits 280 index PRIMARY of table `miza_log`.`blind_date_user_info` trx id 1928467448 lock_mode X locks rec but not gap
Record lock, heap no 186 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
0: len 4; hex 800020fc; asc ;;
1: len 6; hex 000072f213f6; asc r ;;
2: len 7; hex 770000707a116f; asc w pz o;;
3: len 4; hex 80000158; asc X;;
4: len 4; hex 85eaa70f; asc ;;
5: len 4; hex 80000000; asc ;;
6: len 4; hex 80001e46; asc F;;
7: len 4; hex 800003e8; asc ;;
8: len 4; hex 8000001f; asc ;;
9: len 0; hex ; asc ;;
10: len 4; hex 80000000; asc ;;
11: len 4; hex 6017b6b9; asc ` ;;
12: len 4; hex 6017bc1f; asc ` ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 12499 page no 53 n bits 720 index blind_date_id of table `miza_log`.`blind_date_user_info` trx id 1928467448 lock_mode X locks rec but not gap waiting
Record lock, heap no 621 PHYSICAL RECORD: n_fields 2; compact format; info bits 0
0: len 4; hex 80000158; asc X;;
1: len 4; hex 800020fc; asc ;;
*** WE ROLL BACK TRANSACTION (2)
分析
从死锁日志可以知道,当前有两个事务正在执行,而且使用了同一个索引(blind_date_id)。事务1锁住的资源为:
RECORD LOCKS space id 12499 page no 66 n bits 280 index PRIMARY of table `miza_log`.`blind_date_user_info` trx id 1928467447 lock_mode X locks rec but not gap waiting
Record lock, heap no 186 PHYSICAL RECORD: n_fields 13; compact format; info bits 0
值得注意的是:locks res but not gap 代表锁住的是一个索引,不是一个范围
同理,事务2锁住的资源:
RECORD LOCKS space id 12499 page no 66 n bits 280 index PRIMARY of table `miza_log`.`blind_date_user_info` trx id 1928467448 lock_mode X locks rec but not gap
Record lock, heap no 186 PHYSICAL RECORD: n_fields 13;
可以看到,事务2也在和事务1竞争相同的资源。而且两个事务都使用了 X Lock(排他锁,在 《mysql-innodb技术内幕》 锁的章节讲到,X 锁与任何的锁都不兼容),出现相互等待。
事务1中锁住4行记录,事务2锁住3行记录。
问题 1、是什么场景下导致了两个事务并发? 2、事务1和事务2中是更新不同的行,为什么要对索引进行加锁呢?
第一个问题需要我们回到业务场景中去寻找,在业务代码中找到调用这两个sql语句的代码:
在业务中,使用了两个Goroutine去执行了两个语句。所以出现并发的情况并不奇怪。
回到问题2,需要在理解mysql在执行这个语句之前都做了写什么。
首先,在mysql有三种行锁算法:
- Record Lock: 单个行记录上锁
- Gap Lock: 间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock: Gap Lock + Record Lock 锁定一个范围,并且锁定记录本身。
本次涉及的锁是第一种锁Record Lock, 所以本文还是主要针对问题进行分析,至于其他锁和原理,后面会写相应的博客分析。值得注意的是Rcord Lock总是会去锁住索引记录。
innodb的行锁是通过给索引上的索引项加锁实现的,也就是说只有通过索引条件检索数据,innodb才是用行级锁,否则innodb将是用表锁,会把所有扫描过的行都锁定。 在这个案例中,update时使用了两个索引blind_date_id 和 user_id。根据explian语句分析sql可以知道,两个语句都是范围查找,而且最终的使用了user_id这个索引。根据extra标识为using where, innodb 会去扫描返回的行数,然后进行加锁。 具体解释为,事务1为例,首先 innodb 会首先查找 user_id = 99264271的行记录并返回,然后再根据blind_date_id = 344进行扫描进行过滤。在这个过程中,所有扫描的数据都会被锁定。 同理,事务2也是一样,都要对两个索引进行加锁,并在扫描过程也会加锁。最终就导致了相同的行数被两个事务同时加锁,就造成了死锁。
处理方法
经过分析可以知道死锁是由于innodb在扫描中出现的,所以有两个方法:
1.可以对blind_date_id和user_id捡一个联合索引,这样就可以在执行事务过程中使用using index确定到具体的行, 从而避免死锁。
2.
select id form blind_date_user_info where where blind_date_id = 344 and user_id = 2102142;
update blind_date_user_info set hat_num = hat_num + 1 where id in (....);
这样采用先select后update的方法。注意,这里select 不能加for update, 不然还是会造成死锁。
方法1: 直接添加一个索引就可以解决;
方法2: 会出现幻读的情况,但是由于本身业务对于数据的精确度不高,所以最终解决方法采用了第二种。同时只是用一个goroutine执行两个事务。
参考:
《MySQL 技术内幕-InnoDB 存储引擎》第二版,作者:姜承尧
cloud.tencent.com/developer/a…
yq.aliyun.com/articles/33…
dev.mysql.com/doc/refman/…
www.fanyilun.me/2017/04/20/…
才疏学浅,如有错误,请评论指正,不胜感激!