「MySQL高级篇」MySQL为什么会死锁?

1,068 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第十一天,点击查看活动详情

大家好,我是Zhan,一名个人练习时长一年半的大二后台练习生,最近在学MySQL高级篇,现在正在看面经整理遗漏知识点,欢迎各路大佬一起交流讨论

👉本篇速览

在前面对MySQL高级篇的的学习中,我们从存储引擎、索引、锁、SQL语句优化、事务实现、常用工具这些方面学习了MySQL的底层知识,如果有需要了解的可以去看我前面的文章。

谈到锁,死锁一直是一个值得讨论的话题,也是我们需要避免的情况,在了解到锁的使用后,我们就需要考虑什么时候会出现死锁为什么出现死锁、又如何避免死锁,本文将从死锁的出现、死锁的底层原理、死锁的解决方法这几个方面来详细解读MySQL的死锁,当然看本文之前如果不了解三种锁以及查询过程中的锁的构建情况,建议先看这两篇文章:「MySQL高级篇」MySQL全局锁、表级锁、行级锁「MySQL高级篇」你真的知道查询的时候有哪些锁吗?,那么我们就正式开始今天的讲解,今天的讲解将围绕下面四个问题展开,循序渐进:

  • 1️⃣ 死锁是什么呢?(What)
  • 2️⃣ 什么时候会出现死锁呢?(When)
  • 3️⃣ 为什么会出现死锁呢?(Why)
  • 4️⃣ 如何解决死锁的问题呢?(How)

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出现死锁的情况。


📣 下集预告

讲了老半天的锁去解决幻读的问题,那么它真正完全解决了幻读的问题吗,也就是说对于RR事务隔离级别幻读问题是否被完全解决了呢?我们下篇来详细解读,明天见~


🍁 友链


✒写在最后

都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~

求赞.jpeg