前提说明
表结构及初始化数据
CREATE TABLE `ship_info_real` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`mmsi` int(11) NOT NULL,
`pos_time` datetime NOT NULL,
`speed` double DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `mmsi` (`mmsi`),
KEY `pos_time` (`pos_time`)
) ENGINE=InnoDB AUTO_INCREMENT=78 DEFAULT CHARSET=utf8;
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('1', '100', '2020-01-13 16:00:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('48', '200', '2020-03-12 16:10:59', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('47', '200', '2020-03-12 16:00:59', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('46', '200', '2020-03-12 16:00:51', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('45', '200', '2020-03-12 16:19:51', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('43', '200', '2020-03-12 16:19:51', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('42', '200', '2020-03-12 10:19:51', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('40', '200', '2020-03-01 10:19:51', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('39', '200', '2020-03-01 10:19:51', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('49', '200', '2020-03-12 16:10:59', NULL);
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('15', '200', '2020-03-12 16:20:51', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('14', '200', '2020-03-08 11:02:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('5', '200', '2020-03-13 16:52:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('4', '200', '2020-03-13 16:00:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('16', '300', '2020-02-13 16:00:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('7', '300', '2020-03-13 15:00:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('9', '350', '2020-03-13 16:00:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('18', '350', '2020-03-12 16:00:57', '5.2');
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`) VALUES ('10', '400', '2020-03-14 16:01:25', '5.2');
锁知识说明
默认使用mysql的Innodb存储引擎,隔离级别默认采用RR,因为mysql的RR级别可以解决幻读问题,主要通过MVCC+next-key锁解决,MVCC主要是通过快照读实现,next-key主要通过行锁+GAP锁解决,具体知识读者可自行百度。
场景重现
session1
set autocommit=0;
start TRANSACTION;
delete from ship_info_real where mmsi=300; -- step1
-- 阻塞 -- step3
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`)
VALUES ('77', '300', '2020-03-09 16:30:57', '5.2');
COMMIT;
--ROLLBACK;
session2
set autocommit=0;
start TRANSACTION;
delete from ship_info_real where mmsi=310; -- step2
-- 阻塞 -- step4
INSERT INTO `test`.`ship_info_real` (`id`, `mmsi`, `pos_time`, `speed`)
VALUES ('8', '310', '2020-03-13 16:30:57', '5.2');
COMMIT;
-- ROLLBACK;
如果在并发不高的场景下,以上2个session执行肯定没问题,但是并发场景高的情况下就会发生如1,2,3,4步骤一样的情况,死锁截图如下:
原因分析
- 事务A删除mmsi=300,假设这个值存在,那么innodb下的RR隔离级别会产生间隙锁,从满足条件的第一个记录开始
锁住行锁,主键索引锁,间隙锁(前闭后开)。此时准备添加mmsi=300的数据,但是事务B并发开始执行 - 事务B: 删除mmsi=310(不存在的记录),此时没有行锁和主键锁,但同样会上间隙锁(锁住310前后的记录),
然后事务B也添加mmsi=310的数据,此时无法添加因为事务A间隙锁存在 - 结果:事务A insert被阻塞,等待事务B释放间隙锁,而事务B insert也被阻塞,等待事务A的间隙锁释放
- 补充说明:上面场景前提条件是事务B中删除的mmsi不存在时,是非常容易发生死锁问题的;因为篇幅原因,我测试发现事务B删除的mmsi即使存在,也有可能发生死锁问题(只是概率没有不存在的高),只要insert的id在被间隙锁锁住的范围也会死锁,如果是自增的话发生死锁概率较低。
结尾
这个问题之前一直没有找到发生死锁的原因,线上日志时有时无,所以决定写篇博客记录一下,以上如有不正确的地方,欢迎指正并及时修改。