MySQL 死锁了,怎么办?

4f701c4e9b0437114eb14cb8ab162621.jpeg

作者:小林coding

提纲如下:2a87e37c64098579f71ff73f3fc3dd1e.png

正文

有个业务主要逻辑就是新增订单、修改订单、查询订单等操作。然后因为订单是不能重复的,所以当时在新增订单的时候做了幂等性校验,做法就是在新增订单记录之前,先通过 select ... for update 语句查询订单是否存在,如果不存在才插入订单记录。

而正是因为这样的操作,当业务量很大的时候,就可能会出现死锁。

接下来跟大家聊下为什么会发生死锁,以及怎么避免死锁

死锁的发生

本次案例使用存储引擎 Innodb,隔离级别为可重复读(RR)。

接下来,我用实战的方式来带大家看看死锁是怎么发生的。

我建了一张订单表,其中 id 字段为主键索引,order_no 字段普通索引,也就是非唯一索引:

CREATE TABLE `t_order` (`id` int NOT NULL AUTO_INCREMENT,`order_no` int DEFAULT NULL,`create_date` datetime DEFAULT NULL,PRIMARY KEY (`id`),KEY `index_order` (`order_no`) USING BTREE
) ENGINE=InnoDB ;

然后,先 t_order 表里现在已经有了 6 条记录:

f3c74fa0c3dfcf33dd54e6c4df57f805.png
图片

假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:

269a294096eaa4c44cee1057d2cea5f7.png
图片

可以看到,两个事务都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。

这里在查询记录是否存在的时候,使用了 select ... for update 语句,目的为了防止事务执行的过程中,有其他事务插入了记录,而出现幻读的问题。

如果没有使用 select ... for update 语句,而使用了单纯的 select 语句,如果是两个订单号一样的请求同时进来,就会出现两个重复的订单,有可能出现幻读,如下图:

9e49498ba760d0b25b51c5361b557930.png

为什么会产生死锁?

可重复读隔离级别下,是存在幻读的问题。

Innodb 引擎为了解决「可重复读」隔离级别下的幻读问题,就引出了 next-key 锁,它是记录锁和间隙锁的组合。

  • Record Loc,记录锁,锁的是记录本身;

  • Gap Lock,间隙锁,锁的就是两个值之间的空隙,以防止其他事务在这个空隙间插入新的数据,从而避免幻读现象。

普通的 select 语句是不会对记录加锁的,因为它是通过 MVCC 的机制实现的快照读,如果要在查询时对记录加行锁,可以使用下面这两个方式:

begin;
//对读取的记录加共享锁
select ... lock in share mode;
commit; //锁释放begin;
//对读取的记录加排他锁
select ... for update;
commit; //锁释放

行锁的释放时机是在事务提交(commit)后,锁就会被释放,并不是一条语句执行完就释放行锁。

比如,下面事务 A 查询语句会锁住(2, +∞]范围的记录,然后期间如果有其他事务在这个锁住的范围插入数据就会被阻塞。

efcc766862dee53b820e29d29afa2e70.png
图片

next-key 锁的加锁规则其实挺复杂的,在一些场景下会退化成记录锁或间隙锁,我之前也写一篇加锁规则,详细可以看这篇「我做了一天的实验!」

需要注意的是,如果 update 语句的 where 条件没有用到索引列,那么就会全表扫描,在一行行扫描的过程中,不仅给行加上了行锁,还给行两边的空隙也加上了间隙锁,相当于锁住整个表,然后直到事务结束才会释放锁。

所以在线上千万不要执行没有带索引条件的 update 语句,不然会造成业务停滞,我有个读者就因为干了这个事情,然后被老板教育了一波,详细可以看这篇「完蛋,公司被一条 update 语句干趴了!」

回到前面死锁的例子,在执行下面这条语句的时候:

select id from t_order where order_no = 1008 for update;

因为 order_no 不是唯一索引,所以行锁的类型是间隙锁,于是间隙锁的范围是(1006, +∞)。那么,当事务 B 往间隙锁里插入 id = 1008 的记录就会被锁住。

因为当我们执行以下插入语句时,会在插入间隙上再次获取插入意向锁。

Insert into t_order (order_no, create_date) values (1008, now());

插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。而间隙锁与间隙锁之间是兼容的,所以所以两个事务中 select ... for update 语句并不会相互影响

案例中的事务 A 和事务 B 在执行完后 select ... for update 语句后都持有范围为(1006,+∞)的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,导致死锁。

为什么间隙锁与间隙锁之间是兼容的?

在MySQL官网上还有一段非常关键的描述:

Gap locks in InnoDB are “purely inhibitive”, which means that their only purpose is to prevent other transactions from Inserting to the gap. Gap locks can co-exist. A gap lock taken by one transaction does not prevent another transaction from taking a gap lock on the same gap. There is no difference between shared and exclusive gap locks. They do not conflict with each other, and they perform the same function.

这段话表明间隙锁在本质上是不区分共享间隙锁或互斥间隙锁的,而且间隙锁是不互斥的,即两个事务可以同时持有包含共同间隙的间隙锁。

这里的共同间隙包括两种场景:

  • 其一是两个间隙锁的间隙区间完全一样;

  • 其二是一个间隙锁包含的间隙区间是另一个间隙锁包含间隙区间的子集。

间隙锁本质上是用于阻止其他事务在该间隙内插入新记录,而自身事务是允许在该间隙内插入数据的。也就是说间隙锁的应用场景包括并发读取、并发更新、并发删除和并发插入

插入意向锁是什么?

注意!插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁。

在MySQL的官方文档中有以下重要描述:

An Insert intention lock is a type of gap lock set by Insert operations prior to row Insertion. This lock signals the intent to Insert in such a way that multiple transactions Inserting into the same index gap need not wait for each other if they are not Inserting at the same position within the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to Insert values of 5 and 6, respectively, each lock the gap between 4 and 7 with Insert intention locks prior to obtaining the exclusive lock on the Inserted row, but do not block each other because the rows are nonconflicting.

这段话表明尽管插入意向锁是一种特殊的间隙锁,但不同于间隙锁的是,该锁只用于并发插入操作

如果说间隙锁锁住的是一个区间,那么「插入意向锁」锁住的就是一个点。因而从这个角度来说,插入意向锁确实是一种特殊的间隙锁。

插入意向锁与间隙锁的另一个非常重要的差别是:尽管「插入意向锁」也属于间隙锁,但两个事务却不能在同一时间内,一个拥有间隙锁,另一个拥有该间隙区间内的插入意向锁(当然,插入意向锁如果不在间隙锁区间内则是可以的)。

另外,我补充一点,插入意向锁的生成时机:

  • 每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,那 Insert 语句应该被阻塞,并生成一个插入意向锁 。

Insert 语句是怎么加行级锁的?

Insert 语句在正常执行时是不会生成锁结构的,它是靠聚簇索引记录自带的 trx_id 隐藏列来作为隐式锁来保护记录的。

什么是隐式锁?

当事务需要加锁的时,如果这个锁不可能发生冲突,InnoDB会跳过加锁环节,这种机制称为隐式锁。隐式锁是 InnoDB 实现的一种延迟加锁机制,其特点是只有在可能发生冲突时才加锁,从而减少了锁的数量,提高了系统整体性能。

隐式锁就是在 Insert 过程中不加锁,只有在特殊情况下,才会将隐式锁转换为显示锁,这里我们列举两个场景。

  • 如果记录之间加有间隙锁,为了避免幻读,此时是不能插入记录的;

  • 如果 Insert 的记录和已有记录存在唯一键冲突,此时也不能插入记录;

1、记录之间加有间隙锁

每插入一条新记录,都需要看一下待插入记录的下一条记录上是否已经被加了间隙锁,如果已加间隙锁,那 Insert 语句应该被阻塞,并生成一个插入意向锁。

举个例子,现在 t_order 表中,只有这些数据,order_no 是二级索引

3ddcc67a130ef78d07cd886067e4a62f.png

现在,事务 A 执行了下面这条语句。

# 事务 A
mysql> begin;
Query OK, 0 rows affected (0.01 sec)mysql> select * from t_order where order_no = 1006 for update;
Empty set (0.01 sec)

接着,我们执行 select * from performance_schema.data_locks\G; 语句  ,确定事务 A 加了什么类型的锁,这里只关注在记录上加锁的类型。

c1ba2b17897e998f8c3acbbaa74bfcfc.png

可以看到,加的是 X 型得锁,但是具体是记录锁、间隙锁、next-key 锁呢?注意,这里 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思。

首先通过 LOCK_MODE 可以确认是「next-key 锁或者间隙锁」,还是「记录锁」:

  • 如果 LOCK_MODE 为 X,说明是 next-key 锁或者间隙锁;

  • 如果 LOCK_MODE 为 X, REC_NOT_GAP,说明是记录锁。

对于是 next-key 锁,还是间隙锁,就要看  LOCK_DATA 信息。

  • 如果 LOCK_DATA 信息为 supremum,说明是间隙锁;

  • 如果 LOCK_DATA 信息为具体的记录值,说明是 next-key;

因此,本次的例子加的是间隙锁,间隙锁的范围是(1005, +∞)

然后,有个事务 B 在这个间隙锁中,插入了一个记录,那么此时该事务 B 就会被阻塞:

# 事务 B 插入一条记录
mysql> begin;
Query OK, 0 rows affected (0.01 sec)mysql> insert into t_order(order_no, create_date) values(1010,now());
### 阻塞状态。。。。

接着,我们执行 select * from performance_schema.data_locks\G; 语句  ,确定事务 B 加了什么类型的锁,这里只关注在记录上加锁的类型。

55b517ba37508da34963d17c80a2bff2.png

可以看到,事务 B 的状态为等待状态,因为向事务 A 生成的间隙锁 (1005, +∞) 中插入了一条记录,所以事务 B 的插入操作生成了一个插入意向锁(LOCK_MODE: X,INSERT_INTENTION )。

2、遇到唯一键冲突

如果在插入新记录时,插入了一个与「已有的记录的主键或者唯一二级索引列值相同」的记录」(不过可以有多条记录的唯一二级索引列的值同时为NULL,这里不考虑这种情况),此时插入就会失败,然后对于这条记录加上了 S 型的锁

至于是行级锁的类型是记录锁,还是 next-key 锁,跟是主键冲突还是唯一二级索引冲突有关系。

如果主键值重复:

  • 当隔离级别为读已提交时,插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加 S 型记录锁

  • 当隔离级别是可重复读(默认隔离级别),插入新记录的事务会给已存在的主键值重复的聚簇索引记录添加 S 型 next-key 锁

如果唯一二级索引列重复:

  • 不论是哪个隔离级别,插入新记录的事务都会给已存在的二级索引列值重复的二级索引记录添加 S 型 next-key 锁。对的,没错,即使是读已提交隔离级别也是加 next-key 锁,这是读已提交隔离级别中为数不多的给记录添加间隙锁的场景。因为如果不添加间隙锁的话,会让唯一二级索引中出现多条唯一二级索引列值相同的记录,这就违背了 UNIQUE 的约束。

下面举个唯一二级索引冲突的例子,MySQL 8.0 版本,事务隔离级别为可重复读(默认隔离级别)。

t_order 表中的 order_no 字段为唯一二级索引,并且已经存在 order_no 值为 1001 的记录,此时事务 A,插入了 order_no 为 1001 的记录,就出现了报错。

cb89136bd76040b06038eb51c1a13fe4.png

但是除了报错之外,还做一个很重要的事情,就是对 order_no 值为 1001 这条记录加上了 S 型的 next-key 锁

我们可以执行 select * from performance_schema.data_locks\G; 语句  ,确定事务加了什么类型的锁,这里只关注在记录上加锁的类型。

1b1491a251b3e465ab89bc6821359d38.png

可以看到,index_order 二级索引中的 1001(LOCK_DATA) 记录的锁类型为 S 型的 next-key 锁。注意,这里 LOCK_TYPE 中的 RECORD 表示行级锁,而不是记录锁的意思。如果是记录锁的话,LOCK_MODE 会显示 S, REC_NOT_GAP

此时,事务 B 执行了 select * from t_order where order_no = 1001 for update;  就会阻塞,因为这条语句想加 X 型的锁,是与 S 型的锁是冲突的,所以就会被阻塞。

d0982becf1b66491190881d1e7503973.png

我们也可以从 performance_schema.data_locks 这个表中看到,事务 B 的状态(LOCK_STATUS)是等待状态,加锁的类型 X 型的记录锁(LOCK_MODE: X,REC_NOT_GAP    )。

16ee332e3160b83684e02dadfa9e5e1c.png

上面的案例是针对唯一二级索引重复而插入失败的场景。

接下来,分析两个事务执行过程中,执行了相同的 insert 语句的场景。

现在 t_order 表中,只有这些数据,order_no 为唯一二级索引

3b63d628b07d9361d900197266dd32ee.png

在隔离级别可重复读的情况下,开启两个事务,前后执行相同的  Insert 语句,此时事务 B 的  Insert 语句会发生阻塞

c03f47ebc99e2fcff1e0bed7d048af0d.png

两个事务的加锁过程:

  • 事务 A 先插入 order_no 为 1006 的记录,可以插入成功,此时对应的唯一二级索引记录被「隐式锁」保护,此时还没有实际的锁结构;

  • 接着,事务 B 也插入 order_no 为 1006 的记录,由于事务 A 已经插入 order_no 值为 1006 的记录,所以事务 B 在插入二级索引记录时会遇到重复的唯一二级索引列值,此时事务 B 想获取一个 S 型 next-key 锁,但是事务 A 并未提交,事务 A 插入的 order_no 值为 1006 的记录上的「隐式锁」会变「显示锁」且锁类型为  X 型的记录锁,所以事务 B 向获取 S 型 next-key 锁时会遇到锁冲突,事务 B 进入阻塞状态

我们可以执行 select * from performance_schema.data_locks\G; 语句  ,确定事务加了什么类型的锁,这里只关注在记录上加锁的类型。

先看事务 A 对 order_no 为 1006 的记录加了什么锁?从下图可以看到,事务 A  对 order_no 为 1006 记录加上了类型为  X 型的记录锁注意,这个是在执行事务 B 之后才产生的锁,没执行事务 B 之前,该记录还是隐式锁)。

9ce3a3b29dc94b36f2af165e0c1ef4fd.png

然后看事务 B 想对 order_no 为 1006 的记录加什么锁?从下图可以看到,事务 B 想对 order_no 为 1006 的记录加 S 型的 next-key 锁,但是由于事务 A 在该记录上持有了 X 型的记录锁,这两个锁是冲突的,所以导致事务 B 处于等待状态

405eb481086dc4dada4883700c81d99c.png

从这个实验可以得知,并发多个事务的时候,第一个事务插入的记录,并不会加锁,而是会用隐式锁保护唯一二级索引的记录。

但是当第一个事务还未提交的时候,有其他事务插入了与第一个事务相同的记录,第二个事务就会被阻塞因为此时第一事务插入的记录中的隐式锁会变为显示锁且类型是 X 型的记录锁,而第二个事务是想对该记录加上 S 型的 next-key 锁,X 型与 S 型的锁是冲突的,所以导致第二个事务会等待,直到第一个事务提交后,释放了锁。

如果 order_no 不是唯一二级索引,那么两个事务,前后执行相同的  Insert 语句,是不会发生阻塞的,就如前面的这个例子。

61e1ec8157294890eab093e783dc8bf8.png

如何避免死锁?

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。

在数据库层面,有两种策略通过「打破循环等待条件」来解除死锁状态:

  • 设置事务等待锁的超时时间。当一个事务的等待时间超过该值后,就对这个事务进行回滚,于是锁就释放了,另一个事务就可以继续执行了。在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的,默认值时 50 秒。

    当发生超时后,就出现下面这个提示:

313303e3493929a9d31e5994c26fa72e.png
图片
  • 开启主动死锁检测。主动死锁检测在发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑,默认就开启。

    当检测到死锁后,就会出现下面这个提示:

2bc5c9055778a87d55813f2cb078c7ad.png
图片

上面这个两种策略是「当有死锁发生时」的避免方式。

我们可以回归业务的角度来预防死锁,对订单做幂等性校验的目的是为了保证不会出现重复的订单,那我们可以直接将 order_no 字段设置为唯一索引列,利用它的唯一下来保证订单表不会出现重复的订单,不过有一点不好的地方就是在我们插入一个已经存在的订单记录时就会抛出异常。


参考资料:

  • 《MySQL 是怎样运行的?》

  • http://mysql.taobao.org/monthly/2020/09/06/


最后说个段子:

面试官: 解释下什么是死锁?

应聘者: 你录用我,我就告诉你

面试官: 你告诉我,我就录用你

应聘者: 你录用我,我就告诉你

面试官: 卧槽滚!

...........

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/544069.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

10 张图搞懂服务注册发现机制

在微服务架构或分布式环境下,服务注册与发现技术不可或缺,这也是程序员进阶之路必须要掌握的核心技术之一,本文通过图解的方式带领大家轻轻松松掌握。引入服务注册与发现组件的原因先来看一个问题,假如现在我们要做一个商城项目&a…

ASP.NET 5 Beta8 已经发布

Microsoft ASP.NET and Web Tools 2015 (Beta8) http://www.microsoft.com/en-us/download/details.aspx?id49442 .net core 完成了98%,绝大部分类库完成了跨平台开发,已经基本可用,下一版本为RC,发布时间为12月,将可…

面试突击65:HTTPS有什么优点?说一下它的执行流程?

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)说到 HTTPS 相信大部分人都是不陌生,因为目前我们使用的绝大数网站都是基于 HTTPS 的,比如以…

Cell.reuseIdentifier 指什么

Cell.reuseIdentifier 指的是 默认为空,如果不定义,在执行 [_tableView registerNib:templateCellNib forCellReuseIdentifier:_templateCell.reuseIdentifier]; 时,提示 must pass a valid reuse identifier to -[UITableView registerNib:f…

缓存穿透、缓存雪崩、缓存击穿?

背景 在现代软件架构中,缓存的应用已经非常普及。缓存的使用在面试和实践中都是避不开的硬技能、硬知识,如果你说还不太熟悉缓存的使用,可能都不好意思说自己是程序员。这篇文章,带大家进一步学习在缓存使用中不得不考虑三个特殊场…

如何防止订单重复支付?

大家好,我是磊哥,想必大家对在线支付都不陌生,今天和大家聊聊如何防止订单重复支付。看看订单支付流程我们来看看,电商订单支付的简要流程:订单钱包支付流程从下单/计算开始:下单/结算:这一步虽…

3 分钟快速上手 Spring 事件机制

小伙伴们好呀~ 今天来和大家分享下这个 Spring事件机制内容概览image-20210829132019387原理image-20210828184103069这个熟悉 观察者模式 的小伙伴应该一眼就看出来啦~其实就是个简单版的 发布-订阅模式有三个核心类👇事件 ApplicationEvent事件发布器 Application…

mis dss gis_MIS中的决策支持系统(DSS)

mis dss gisThe Decision Support System is always helpful to management people to take decisions/decisions and finds the key business insights from available information systems. 决策支持系统始终有助于管理人员做出决策/决策,并从可用的信息系统中找到…

使用Grunt构建自动化开发环境

1、准备工作 1)首页确保电脑上网,以及能够访问https://registry.npmjs.org/,因需从此网站中下载安装相应的插件; 2)电脑安装Node.js,Grunt及Grunt插件都是基于node.js运行的;如果你电脑上未装node.js&#…

面试突击66:请求转发和请求重定向有什么区别?

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)在 Java 中,跳转的实现方式有两种:请求转发和请求重定向,但二者是完全不同的&…

99%的Java程序员会踩的6个坑

前言作为Java程序员的你,不知道有没有踩过一些基础知识的坑。有时候,某个bug,你查了半天,最后发现竟然是一个非常低级的错误。有时候,某些代码,这一批数据功能正常,但换了一批数据就出现异常了。…

BigDecimal 的 4 个坑,你踩过几个?

背景 一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多同学因为不知道、不了解或使用不当导致资损事件发生。所以,如果你从事金融相关项目,或者你的项目中涉及到金额的计算,那么你一定要花时间看看这篇…

Windows Server 2012 R2 里面如何安装Net Framework 3.5

图示 不要慌,和windows是不一样的,没有问题 下一步 默认即可,下一步 这里面的东西以后会装,先不管,我们今天目的是装 net framework 3.5 选一下 正在安装 如果出错了请参考: http://www.2cto.com/os/201410…

聊聊Java中代码优化的30个小技巧

今天我们一起聊聊Java中代码优化的30个小技巧,希望会对你有所帮助。1.用String.format拼接字符串不知道你有没有拼接过字符串,特别是那种有多个参数,字符串比较长的情况。比如现在有个需求:要用get请求调用第三方接口,…

面试突击69:TCP 可靠吗?为什么?

作者 | 磊哥来源 | Java面试真题解析(ID:aimianshi666)转载请联系授权(微信ID:GG_Stone)相比于 UDP 来说,TCP 的主要特性是三个:有连接、可靠、面向数据流。所谓的“有连接”指的是 …

Java 是值传递还是引用传递?

作者 | 王磊来源 | Java中文社群(ID:javacn666)转载请联系授权(微信ID:GG_Stone)开篇先来曝答案,在 Java 语言中,本质只有值传递,而无引用传递,解释和证明详见…

SpringCloud基于RocketMQ实现分布式事务

前言分布式事务是在微服务开发中经常会遇到的一个问题,之前的文章中我们已经实现了利用Seata来实现强一致性事务,其实还有一种广为人知的方案就是利用消息队列来实现分布式事务,保证数据的最终一致性,也就是我们常说的柔性事务。消…

漫画:怎么证明sleep不释放锁,而wait释放锁?

wait 加锁示例public class WaitDemo {private static Object locker new Object();public static void main(String[] args) throws InterruptedException {WaitDemo waitDemo new WaitDemo();// 启动新线程,防止主线程被休眠new Thread(() -> {try {waitDemo…

就国内某个程序员问答网站的简单的分析

为什么80%的码农都做不了架构师?>>> 一、数据抓取 分析页面数据,设计数据表结构数据只要包含投票、回答数、问题状态、最后谁回答过、浏览数、问题标题、标签,数据样例如下:由于一开只打算爬问题标题,问题…

iOS开发中 常用枚举和常用的一些运算符(易错总结)

1、色值的随机值:#define kColorValue arc4random_uniform(256)/255.0 // arc4random_uniform(256)/255.0; 求出0.0~1.0之间的数字view.backgroundColor [UIColor colorWithRed:kColorValue green: kColorValue blue: kColorValue alpha: 0.5]; 2、定时器的使用&…