MySQL为什么会死锁?

码农老张 后端 2022-12-06

1️⃣ 死锁概述

在正式开始今天的讲解之前,我们先回顾一下死锁的相关知识

死锁是指两个或者两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而导致的一种阻塞的现象,如果没有外力,他们将一直等待下去。

就跟卡bug一样,比如说你去面试,面试官问你:MySQL为什么会死锁;你告诉面试官:你录用我我就告诉你,面试官说:你告诉我我就录用你,然后你两就一直这么你问我我问你,这就是死锁。

那么,什么时候会发生死锁呢?

这就不得不提死锁的四个必要条件:互斥、占有并等待、非抢占、循环等待

  1. 互斥:也就是说至少有一个资源处于独占的状态,也就是说不能被两个线程同时使用

  2. 占有并等待:一个进程至少占有一个资源,并且等待另一个资源,但是这个资源被别人占有了

  3. 非抢占式:咱就是说线程不能去抢占别的线程占有的资源,只能自己主动释放

  4. 循环等待:A等着B的资源,B等着C的资源,C又等着A的资源


2️⃣ 死锁的发生

现在我们模拟一个死锁的场景,在此之前我们先创建一张团队表用于存储一个排球队的人员信息:

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 = 6position_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给我们提供了两种策略来打破死锁:

  1. 设置事务等待锁的超时时间,也就是说如果事务中一直阻塞,在超过设置的innodb_lock_wait_timeout做个参数的值之后,可以让事务超过指定时间后自动回滚并释放锁

  2. 开启主动死锁检测:这是MySQL提供的死锁检测,如果这个机制发现了死锁,就会回滚其中的一个事务,让其他的事务得到执行,那么所有的事务就都解开了,设置的方法为:innodb_deadlock_detect = on即可

业务层面,我们在处理业务逻辑的时候,主动的去寻找死锁存在的可能性,从根源解决问题,并加以修正,比如如果是防止订单号重复,也就是防止查重,我们可以修改订单号的生成规则,以雪花算法或者Redis生成订单号,或者说可以给订单号这个字段加上唯一的索引……


💬 总结

今天的文章只是带大家简单走了一遍MySQL的死锁情况,关于为什么会死锁,讲完昨天的文章,也就是对查询时的加锁情况的讲解,其实来分析这个死锁的情况并不是一件难事,最后也介绍了从数据库层面业务层面如何去防止MySQL出现死锁的情况。

Apipost 私有化火热进行中

评论