生产环境GAP锁引发的死锁问题分析

前提说明

表结构及初始化数据

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步骤一样的情况,死锁截图如下:

原因分析

  1. 事务A删除mmsi=300,假设这个值存在,那么innodb下的RR隔离级别会产生间隙锁,从满足条件的第一个记录开始
        锁住行锁,主键索引锁,间隙锁(前闭后开)。此时准备添加mmsi=300的数据,但是事务B并发开始执行
  2. 事务B: 删除mmsi=310(不存在的记录),此时没有行锁和主键锁,但同样会上间隙锁(锁住310前后的记录),
        然后事务B也添加mmsi=310的数据,此时无法添加因为事务A间隙锁存在
  3. 结果:事务A insert被阻塞,等待事务B释放间隙锁,而事务B insert也被阻塞,等待事务A的间隙锁释放
  4. 补充说明:上面场景前提条件是事务B中删除的mmsi不存在时,是非常容易发生死锁问题的;因为篇幅原因,我测试发现事务B删除的mmsi即使存在,也有可能发生死锁问题(只是概率没有不存在的高),只要insert的id在被间隙锁锁住的范围也会死锁,如果是自增的话发生死锁概率较低。

结尾

这个问题之前一直没有找到发生死锁的原因,线上日志时有时无,所以决定写篇博客记录一下,以上如有不正确的地方,欢迎指正并及时修改。

发布了1 篇原创文章 · 获赞 2 · 访问量 38

猜你喜欢

转载自blog.csdn.net/huierlv/article/details/105342327