事故背景
生产环境稳定运行了很久的系统,收到了测试提出的诡异bug,说添加数据很慢,触发了前端的超时,而且还是偶现,这就很郁闷了,生产环境没有出现过,只在测试环境出现过几次,面对这个 bug,我有点头痛,偶现的bug最重要的就是复现问题,然后才能解决,由于在开发环境尝试了很多次之后没有出现,就没有放心上,将bug的状态挂起,备注为持续观察。
直到有一次,测试部门收到需要对接口做性能测试,然后在他们的性能测试中,这个诡异的bug就成了必现问题,在并发情况下,会有大量的线程处于阻塞问题,并且服务器日志显示数据库存在死锁。
如何产生的死锁
添加数据时,存在一张关联表,会对关联表添加对应的数据,但是添加数据之前需要将原来的数据删除,但是删除、插入都有索引,是不可能导致死锁呢?那到底是什么导致数据库死锁了呢?
打开程序的 sql 日志,发现死锁确实存在插入语句中,这就让我更纳闷了,插入的都是新数据,怎么会存在死锁呢?难道是前面的删除导致了插入获取锁失败吗?
问题定位
为了模拟问题,新建一张数据表作为测试。
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL,
`age` smallint(6) NOT NULL,
`sex` tinyint(1) DEFAULT NULL,
`mail` varchar(128) DEFAULT NULL,
`created_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
) ENGINE=InnoDB AUTO_INCREMENT=1796486 DEFAULT CHARSET=utf8mb4;
新建了一张用户表,并且新增两条数据
INSERT INTO `sys`.`sys_user`(`id`, `username`, `age`, `sex`, `mail`, `created_time`) VALUES (1, '李四', 33, 1, '123@qq.com', now());
INSERT INTO `sys`.`sys_user`(`id`, `username`, `age`, `sex`, `mail`, `created_time`) VALUES (2, '张三', 33, 1, '123@qq.com', now());
为了验证我的猜想,这里模拟一下删除和新增的逻辑,开启两个事务A、B,A事务负责插入数据,B事务负责删除数据(为什么插入和删除不在一个事务?只有不同事物才会有可能造成死锁,如果两个事务都是插入,除了插入相同的数据,不然也不可能死锁)。
-- 事务 A
start transaction with consistent snapshot;
INSERT INTO `sys`.`sys_user`(`id`, `username`, `age`, `sex`, `mail`, `created_time`) VALUES (3, '张三', 33, 1, '123@qq.com', '2022-03-31 20:00:34');
commit;
start transaction with consistent snapshot;
delete from sys_user where id = 4;
commit;
同时开始A、B事务,先执事务B的删除操作,删除事务B中 id为4的用户数据,不要提交B事务,然后再插入事务A中 id 为3 的数据(因为两个事务操作的是不同的数据)。
但是结果并没有达到我的预期,我的预期是事务 A 的插入操作会被阻塞,因为程序中就是这样的,但是执行结果却是事务 A 的插入操作并没有被阻塞,而是直接插入成功。
所以得出结论,并不是删除导致插入的死锁,这里也符合mysql的锁原则,id是主键,操作它都是行数,id 3、4互不影响。
这里就让我郁闷了,既然不是删除影响的,那会是什么影响的呢?正当我一筹莫展的时候,我发现,程序中的逻辑是这样的,会经常删除一些重复的数据,比如,我刚添加了id为3的数据,下次我就把他删除了又插入了一条 id 为3的数据,本来是做更新的,开发人员为了偷懒就直接将原来的数据删除然后再次添加。
到这里我可能猜到了问题所在,会不会某个id已经被删除的数据,事务B去删除,然后事务A添加数据的时候正好使用到了这个id,这样两个事务同时操作一条数据,但是删除一条不存在的数据,也会被加锁吗?
尝试修改ssql语句
-- 事务 A
start transaction with consistent snapshot;
INSERT INTO `sys`.`sys_user`(`id`, `username`, `age`, `sex`, `mail`, `created_time`) VALUES (6, '张三', 33, 1, '123@qq.com', now());
commit;
start transaction with consistent snapshot;
delete from sys_user where id = 6;
commit;
终于,死锁出现了,事务A的插入操作发生死锁了,但是id为 6 的数据在数据库并不存在 ,这也就说明了及时在数据库中删除或更新一条不存在的数据,同样会存在锁。
总结
死锁原理
删除和更新同一条数据导致死锁,注意点:即使操作的是数据库不存在的数据,同样会导致锁的存在,程序中因为id的重复使用,所以导致了再多线程的压力下,出现了删除、插入相同的id,而这个id就是程序中不存在的数据(出现这种问题,代码写的有拉跨)。
建议
在更新、删除操作前,请先判断一下是否存在数据在进行,编写代码时一定要严谨,不要抱有侥幸心里。