1️⃣ 死锁概述
在正式开始今天的讲解之前,我们先回顾一下死锁的相关知识
死锁是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而导致的一种阻塞的现象,如果没有外力,他们将一直等待下去。
就跟卡bug一样,比如说你去面试,面试官问你:MySQL为什么会死锁;你告诉面试官:你录用我我就告诉你,面试官说:你告诉我我就录用你,然后你两就一直这么你问我我问你,这就是死锁。
那么,什么时候会发生死锁呢?
这就不得不提死锁的四个必要条件:互斥、占有并等待、非抢占、循环等待
- 互斥:也就是说至少有一个资源处于独占的状态,也就是说不能被两个线程同时使用
- 占有并等待:一个进程至少占有一个资源,并且等待另一个资源,但是这个资源被别人占有了
- 非抢占式:咱就是说线程不能去抢占别的线程占有的资源,只能自己主动释放
- 循环等待:A等着B的资源,B等着C的资源,C又等着A的资源
2️⃣ 死锁的发生
现在我们模拟一个死锁的场景,在此之前我们先创建一张团队表用于存储一个排球队的人员信息:
代码语言:javascript复制CREATE TABLE `team` ( `id` int NOT NULL AUTO_INCREMENT, `position_no` int DEFAULT NULL, `user_name` VARCHAR(10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_position` (`position_no`) USING BTREE ) ENGINE=InnoDB ; 复制代码
并插入五行数据:
正在上传…重新上传取消
现如今球队招募一个二传手和一个副攻手,也就是position_no = 6 和 position_no = 7的人,但是在招募之前需要确认这个位置是否有人,球队的两个经理往表中插入数据的时候发生如下的事情,此时就会发生死锁的情况:
经理A(清水) | 经理B(谷地) |
---|---|
查询位置为6的人员是否存在 select * from team where position_no = 6 for update | |
查询位置为7的是否存在人员 select * from team where position_no = 7 for update | |
insert into team(position_no, user_name) values(6,研磨) | |
insert into team(position_no, user_name) values(7,列夫) |
首先解释一下为什么使用的是当前读,因为如果是快照读就会出现的情况为:两个经理都要插入位置为6的人员,并且查询的时候都发现位置存在,就都进行了插入,即:
经理A(清水) | 经理B(谷地) |
---|---|
查询位置为6的人员是否存在 select * from team where position_no = 6 for update | |
查询位置为7的是否存在人员 select * from team where position_no = 6 for update | |
insert into team(position_no, user_name) values(6,研磨) | |
insert into team(position_no, user_name) values(6,及川) |
此时就会发生两个人员都被加入进来了,出现了两个位置为6的记录,出现了幻读,因此在查询的时候需要加锁,也就是使用当前读
回到刚刚的场景,我们去实际环境试一下会得到的结果是:经理A插入位置为6的人员,经理B插入位置为7的人员的时候,这两句插入都阻塞了,也就是发生了死锁,在下面我们会分析为什么出现死锁:
3️⃣ 死锁的底层原理分析
其实有了昨天的知识储备,了解了查询的时候的加锁情况,我们其实不难分析出为什么会死锁:
select * from team where position_no = 6 for update语句属于非唯一索引的等值查询,会加上(6, ∞]的临键锁
select * from team where position_no = 7 for update语句属于非唯一索引的等值查询,会加上(7, ∞]的临键锁
两个事务都持有范围为(6, ∞]的临键锁,而接下来的插入操作会去获取插入意向锁,插入意向锁与临键锁互斥,因此获取插入意向锁需要对方的事务的临键锁释放,于是就出现了循环等待,也就是死锁
4️⃣ 如何避免死锁
在数据库层面,MySQL给我们提供了两种策略来打破死锁:
- 设置事务等待锁的超时时间,也就是说如果事务中一直阻塞,在超过设置的innodb_lock_wait_timeout做个参数的值之后,可以让事务超过指定时间后自动回滚并释放锁
- 开启主动死锁检测:这是MySQL提供的死锁检测,如果这个机制发现了死锁,就会回滚其中的一个事务,让其他的事务得到执行,那么所有的事务就都解开了,设置的方法为:innodb_deadlock_detect = on即可
在业务层面,我们在处理业务逻辑的时候,主动的去寻找死锁存在的可能性,从根源解决问题,并加以修正,比如如果是防止订单号重复,也就是防止查重,我们可以修改订单号的生成规则,以雪花算法或者Redis去生成订单号,或者说可以给订单号这个字段加上唯一的索引……