MySQL - 死锁的产生及解决方案

  • 1. 死锁与产生死锁的四个必要条件
    • 1.1 什么是死锁
    • 1.2 死锁产生的4个必要条件
  • 2. 死锁案例
    • 2.1 表锁死锁
    • 2.2 行锁死锁
    • 2.3 共享锁转换为排他锁
  • 3. 死锁排查
  • 4. 实例分析
    • 4.1 案例描述
    • 4.2 案例死锁问题复现
    • 4.3 死锁排查
    • 4.4 解决死锁
  • 5. 如何避免死锁

1. 死锁与产生死锁的四个必要条件

1.1 什么是死锁

死锁是指2+进程执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

摘自:@百度百科

1.2 死锁产生的4个必要条件

虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:

1)互斥条件: 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

2)请求和保持条件: 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

3)不剥夺条件: 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

4)环路等待条件: 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

我们强调所有四个条件必须同时成立才会出现死锁。环路等待条件意味着占有并等待条件,这样四个条件并不完全独立。

死锁的关键在于:2+ 的 session 加锁的顺序不一致。

那么对应的解决死锁问题的关键就是:让不同的 session 加锁有次序。

2. 死锁案例

2.1 表锁死锁

  • 产生原因:

用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要等用户A释放表A才能继续,这就死锁就产生了。

用户A–》A表(表锁)–》B表(表锁)
用户B–》B表(表锁)–》A表(表锁)

  • 解决方案:

这种死锁比较常见,是由于程序的 BUG 产生的,除了调整的程序的逻辑没有其它的办法。仔细分析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。

2.2 行锁死锁

  • 产生原因1:

如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发生阻塞或死锁。

  • 解决方案1:

SQL 语句中不要使用太复杂的关联多表的查询;使用 explain “执行计划"对 SQL 语句进行分析,对于有全表扫描和全表锁定的 SQL 语句,建立相应的索引进行优化。

  • 产生原因2:

两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁。

  • 解决方案2:

(1)在同一个事务中,尽可能做到一次锁定所需要的所有资源;
(2)按照 id 对资源排序,然后按顺序进行处理。

2.3 共享锁转换为排他锁

  • 产生原因:

事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 再执行更新操作时,此处发生死锁,因为事务A 需要排他锁来做更新操作。但是,无法授予该锁请求,因为事务B 已经有一个排他锁请求,并且正在等待事务A 释放其共享锁。

事务A:

-- 共享锁,1
select * from dept where deptno=1 lock in share mode;
-- 排他锁,3
update dept set dname='java' where deptno=1;

事务B:

-- 由于1有共享锁,没法获取排他锁,需等待,2
update dept set dname='Java' where deptno=1;
  • 解决方案:

(1)对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操作;

(2)使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。

3. 死锁排查

MySQL 提供了几个与锁有关的参数和命令,可以辅助我们优化锁操作,减少死锁发生。

  1. 查看死锁日志:

通过 show engine innodb status \G 命令查看近期死锁日志信息,主要关注日志中的 LATEST DETECTED DEADLOCK 部分;

使用方法:

(1)查看近期死锁日志信息;
(2)使用 explain 查看下 SQL 执行计划。

  1. 查看锁状态变量

通过 show status like 'innodb_row_lock%' 命令检查状态变量,分析系统中的行锁的争夺情况

  • Innodb_row_lock_current_waits:当前正在等待锁的数量
  • Innodb_row_lock_time:从系统启动到现在锁定总时间长度
  • Innodb_row_lock_time_avg: 每次等待锁的平均时间
  • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间
  • Innodb_row_lock_waits:系统启动后到现在总共等待的次数

如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后着手定制优化。

4. 实例分析

4.1 案例描述

本次发生死锁的是库存扣减接口,该接口的主要逻辑是用户下单后,扣减订单商品在某个仓库的库存量。比如用户一个在vivo官网下单买了1台X50手机和1台X30耳机,那么下单后,首先根据用户收货地址确定发货仓库,然后从该仓库里面分别减去一个X50库存和一个X30库存。分析死锁sql之前,先看下商品库存表的定义(为方便理解,只保留主要字段):

CREATE TABLE `store` (`id` int(10) AUTO_INCREMENT COMMENT '主键',`sku_code` varchar(45) COMMENT '商品编码',`ws_code` varchar(32) COMMENT '仓库编码',`store` int(10) COMMENT '库存量',PRIMARY KEY (`id`),KEY `idx_skucode` (`sku_code`),KEY `idx_wscode` (`ws_code`)) ENGINE=InnoDB COMMENT='商品库存表'

注意这里分别给 sku_code 和 ws_code 两个字段单独定义了索引:idx_skucode,idx_wscode。这样做的原因主要是业务上有根据单个字段查询的要求。

4.2 案例死锁问题复现

再看下库存扣减update语句:

update store
set store = store-#{store}
where sku_cod e= #{skuCode} and ws_code = #{wsCode} and (store-#{store}) >= 0;

这个 SQL 的业务含义就是对某个商品(skuCode)从某个仓库(wsCode)中扣减 store 个库存量,同时上面的 where 条件同时出现了 sku_code 和 ws_code 字段,压测数据中 sku_code 的选择度要比 ws_code 高,理论上这条 SQL 应该会走 idx_skucode 索引,那么真实情况是怎样的呢?

好,接下来对库存扣减接口卡进行压测,50的并发,每个订单5个商品,刚压不到半分钟就出现了死锁,再压,问题依旧,说明是必现的问题,必现解决后才能继续。

4.3 死锁排查

通过 show engine innodb status \G 命令查看近期死锁日志信息,主要关注日志中的 LATEST DETECTED DEADLOCK 部分:

-----------------------LATEST DETECTED DEADLOCK------------------------2020-xx-xx 21:09:05 7f9b22008700*** (1) TRANSACTION:TRANSACTION 4219870943, ACTIVE 0 sec fetching rowsmysql tables in use 3, locked 3LOCK WAIT 10 lock struct(s), heap size 2936, 3 row lock(s)MySQL thread id 301903552, OS thread handle 0x7f9b21a7b700, query id 5373393954 10.101.22.135 root updatingupdate storeset update_time = now(), store = store-1where sku_code='5468754' and ws_code = 'NO_001' and (store-1) >= 0*** (1) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 3331 page no 16 n bits 904 index `idx_wscode` of table `store` trx id 4219870943 lock_mode X locks rec but not gap waitingRecord lock, heap no 415 PHYSICAL RECORD: n_fields 2; compact format; info bits 00: len 5; hex 5730303735; asc NO_001;;1: len 8; hex 00000000000025a7; asc % ;;*** (2) TRANSACTION:TRANSACTION 4219870941, ACTIVE 0 sec fetching rows, thread declared inside InnoDB 1mysql tables in use 3, locked 39 lock struct(s), heap size 2936, 4 row lock(s)MySQL thread id 301939956, OS thread handle 0x7f9b22008700, query id 5373393941 10.101.22.135 root updatingupdate storeset update_time = now(), store = store-1where sku_code='5655620' and ws_code = 'NO_001' and (store-1) >= 0*** (2) HOLDS THE LOCK(S):RECORD LOCKS space id 3331 page no 16 n bits 904 index `idx_wscode` of table `store` trx id 4219870941 lock_mode X locks rec but not gapRecord lock, heap no 415 PHYSICAL RECORD: n_fields 2; compact format; info bits 00: len 5; hex 5730303735; asc NO_001;;1: len 8; hex 00000000000025a7; asc % ;;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:RECORD LOCKS space id 3331 page no 7 n bits 328 index `PRIMARY` of table `store` trx id 4219870941 lock_mode X locks rec but not gap waitingRecord lock, heap no 72 PHYSICAL RECORD: n_fields 9; compact format; info bits 00: len 8; hex 00000000000025a7; asc % ;;1: len 6; hex 0000fb85fdf7; asc ;;2: len 7; hex 1a00001d3b21d4; asc ;! ;;3: len 7; hex 35343638373534; asc 5468754;;4: len 5; hex 5730303735; asc NO_001;;5: len 8; hex 8000000000018690; asc ;;6: len 5; hex 99a76b2b97; asc k+ ;;7: len 5; hex 99a7e35244; asc RD;;8: len 1; hex 01; asc ;;

从上面日志可以看出,存在两个事务,分别在执行这两条sql时发生了死锁:

update store set update_time = now(), store = store-1 where sku_code='5468754' and ws_code = 'NO_001' and (store-1) >= 0;update store set update_time = now(), store = store-1 where sku_code='5655620' and ws_code = 'NO_001' and (store-1) >= 0;

看一下实际数据:


就是说,这两个事务在更新同一张表的不同行时发生了死锁。在我们直观印象里,innodb使用的是行锁,不同的行锁之间应该是互不干扰的?那这是怎么一回事呢?

和我们想象的不同,InnoDB 既没有使用 idx_skucode 索引,也没有使用 idx_wscode 索引,而是使用了 index_mergeindex_merge 和这两个索引是什么关系呢?

查询资料得知 index_merge 是 MySQL 5.1 后引入的一项索引合并优化技术,它允许对同一个表同时使用多个索引进行查询,并对多个索引的查询结果进行合并(取交集(intersect)、并集(union)等)后返回。

回到上面的 update 语句:where sku_code='5468754' and ws_code = 'NO_001' ;如果没有 index_merge,要么走 idx_skucode 索引,要么走 idx_wscode 索引,不会出现两个索引一起使用的情况。而在使用 index_merge 技术后,会同时执行两个索引,分别查到结果后再进行合并(where条件是and,所以会做交集运算)。再结合第二部分对加锁机制(分步按记录加锁)的理解,是否隐约觉得两个索引的同时加锁是导致死锁的原因呢?

我们再深入死锁日志看一下,日志比较复杂,翻译过来大意如下:

  • 事务一 4219870943 在执行update语句时,在等待索引idx_wscode上的行锁(编号space id 3331 page
    no 16 n bits 904 )。
  • 事务二 4219870941 在执行update语句时,已经持有idx_wscode上的行锁(编号space id 3331 page
    no 16 n bits 904 ),从锁编号来看,就是事务一需要的锁。
  • 事务二 4219870941 同时也在等待主键索引上的一把锁,这把锁谁在持有呢?从这行日志(3: len 7; hex 35343638373534; asc 5468754;;)可以看出,正是事务一要更新的那行记录,说明这把锁被事务一霸占着。

好了,死锁条件已经很清楚了:事务一在等待事务二持有的索引 idx_wscode 上的行锁(编号space id 3331 page no 16 n bits 904 ),而事务二同时也在等待事务一持有的主键索引(5468754)上的锁,大家互不相让,只能僵在那里死锁。

用一张图来说明一下这个情况:

上图描述的只是发生死锁的一条可能路径,实际上仔细梳理的话还有其他路径也会导致死锁,大家感兴趣可以自己探索。上图解释如下:

  • 事务一(where sku_code=‘5468754’ and ws_code = ‘NO_001’)首先走idx_skucode索引,分别对二级索引和主键索引加锁成功(1-1和1-2);

  • 此时事务二开始执行( where sku_code=‘5655620’ and ws_code = ‘NO_001’),首先也是走idx_skucode(左上)索引,因为和事务一所加锁的记录不冲突,所以也顺利加锁成功(2-1和2-2);

  • 事务二继续执行,这时走的是idx_wscode(右上)索引,先对二级索引加锁成功(2-3,此时事务一还没有开始在idx_wscode上加锁),但是在对主键索引加索引时,发现id=9639的主键索引已经被事务一上锁,因此只能等待(2-4),同时在2-4完成加锁前,对其他记录的加锁也会暂停(2-5和2-6,因为InnoDB是逐条记录加锁的,前一条未完成则后面的不会执行);

  • 此时事务一继续执行,这时走的是idx_wscode索引,但是加锁的时候发现(NO_001,9639)这条索引项已经被事务二上锁,所以也只能等待。同理,后面的1-4也无法执行。

到此就出现了“两个事务,反向加锁"导致的死锁现象。

4.4 解决死锁

死锁的本质原因还是由加锁顺序不同所导致,本例中是由于Index Merge同时使用2个索引方向加锁所导致,解决方法也比较简单,就是消除因index merge带来的多个索引同时执行的情况。

1)利用 force index(idx_skucode) 强制走某个索引,这样 InnoDB 就会忽略index merge,避免多个索引同时加锁的情况。

2)禁用 Index Merge,这样 InnoDB 只会使用 idx_skucode 和 idx_wscode 中的一个,所有事物加锁顺序都一样,不会造成死锁。

用命令禁用Index Merge:

SET GLOBAL  optimizer_switch='index_merge=off, index_merge_union=off, index_merge_sort_union=off, index_merge_intersection=off';


重新登录终端后再看下执行计划:

3)既然Index Merge同时使用了2个独立索引,我们不妨新建一个包含这两个索引所有字段的联合索引,这样InnoDB就只会走这个单独的联合索引,这其实和禁用index merge是一个道理。

新增联合索引:

alter table store add index idx_skucode_wscode(sku_code, ws_code);

再看下执行计划,type = range 说明没有使用 index merge,另外 key = idx_skucode_wscode 说明走的是刚刚创建的联合索引:

4)最后推荐另外一种绕过 index merge 限制的方式。即去除死锁产生的条件,具体方法是先利用 idx_skucode 和 idx_wscode 查询到主键 id,再拿主键 id 进行 update 操作。这种方式避免了由 update 引入X锁,由于最终更新的条件是唯一固定的,所以不存在加锁顺序的问题,避免了死锁的产生。

案例来源:@dbaplus社群

5. 如何避免死锁

  • 事务尽可能小,不要将复杂逻辑放进一个事务里。
  • 涉及多行记录时,约定不同事务以相同顺序访问。
  • 业务中要及时提交或者回滚事务,可减少死锁产生的概率。
  • 表要有合适的索引。
  • 可尝试将隔离级别改为 RC 。

MySQL - 死锁的产生及解决方案相关推荐

  1. mysql死锁解决方法_mysql出现死锁的原因及解决方案

    mysql出现死锁的原因及解决方案 发布时间:2020-06-04 16:35:40 来源:51CTO 阅读:418 作者:三月 本文主要给大家介绍mysql出现死锁的原因及解决方案,文章内容都是笔者 ...

  2. mysql死锁解决方法_MySQL死锁及解决方案

    一.MySQL锁类型 1. MySQL常用存储引擎的锁机制 MyISAM和MEMORY采用表级锁(table-level locking) BDB采用页面锁(page-level locking)或表 ...

  3. mysql查询死锁的次数_一次神奇的MySQL死锁排查记录

    一次神奇的MySQL死锁排查记录 发布时间:2020-08-29 00:50:26 来源:脚本之家 阅读:135 作者:咖啡拿铁 背景 说起Mysql死锁,之前写过一次有关Mysql加锁的基本介绍,对 ...

  4. MySQL死锁如何处理

    转载自  MySQL死锁如何处理 前提 笔者负责的一个系统最近有新功能上线后突然在预警模块不定时报出MySQL死锁导致事务回滚.幸亏,上游系统采用了异步推送和同步查询结合的方式,感知到推送失败及时进行 ...

  5. mysql 死锁监视器_并发基础知识:死锁和对象监视器

    mysql 死锁监视器 本文是我们名为Java Concurrency Essentials的学院课程的一部分. 在本课程中,您将深入探讨并发的魔力. 将向您介绍并发和并发代码的基础知识,并学习诸如原 ...

  6. mysql死锁语句_记一次神奇的Mysql死锁排查

    背景 说起Mysql死锁,之前写过一次有关Mysql加锁的基本介绍,对于一些基本的Mysql锁或者死锁都有一个简单的认识,可以看下这篇文章为什么开发人员需要了解数据库锁.有了上面的经验之后,本以为对于 ...

  7. MySQL死锁解决之道

    转载自知乎 云之飞舞 真大佬 一. 了解常见的锁类型 在讨论传统的隔离级别实现的时候,我们就提到:通过对锁的类型(读锁还是写锁),锁的粒度(行锁还是表锁),持有锁的时间(临时锁还是持续锁)合理的进行组 ...

  8. mysql死锁介绍以及解决

    什么是死锁 死锁是2+个线程在执行过程中, 因争夺资源而造成的相互等待的现象,若无外力作用,它们将无法推进下去. 死锁产生的4个必要条件 互斥条件 指进程对所分配的资源进行排他性使用,即一段时间内某资 ...

  9. java mysql死锁_记一次线上mysql死锁分析(一)

    记录一次比较诡异的mysql死锁日志.系统运行几个月来,就在前几天发生了一次死锁,而且就只发生了一次死锁,整个排查过程耗时将近一天,最后感谢我们的DBA大神和老大一起分析找到原因. 诊断死锁 借助于我 ...

最新文章

  1. vs2010 使用vs online账号 需要安装的插件
  2. linux 把mysql大小写关闭_linux中设置mysql大小写不去区分方法
  3. 三个免费图片网站:特别适合场景图
  4. QT5.4 vs2013静态加载插件的sqlite静态编译
  5. python 多进程与多线程配合拷贝文件目录
  6. 08、单链表编程考点
  7. eclipse中SSH三大框架环境搭建二
  8. 去360总部参加网络信息安全会议经历
  9. 饿了么监控系统 EMonitor 与美团点评 CAT 的对比
  10. java DTO循环_Java Stream与for循环比较
  11. kali linux 自动登录,Kali Linux SSH登录故障处理
  12. js生成二维码 中间有logo
  13. 软考软件设计师考试总结(2019下半年)
  14. 3d打印机品牌排行榜揭晓,stratasys公司名列前茅
  15. Chrome浏览器各种崩溃、卡死解决方法
  16. 如何在家自学编程成为一名程序员?
  17. Codeforces Round #744 (Div. 3) B. Shifting Sort
  18. 通过数据分析,了解外国人眼里的真实李子柒
  19. 转:探寻问题背后的问题——提问的4个正确姿势
  20. GICv3-4零散的寄存器解读(1)

热门文章

  1. Pycharm连接SQLSever
  2. 怎样基于VitePress(Vite官网主题)写自己文档
  3. Netty框架之网络通信入门程序~helloWorld
  4. QT中QString 和 LPCWSTR 的相互转换
  5. 【Arduino】这么方便?舵机快捷调试软件分享。
  6. Java经典算法50题(含代码)
  7. 【java面试】多线程如果线程挂住了怎么办
  8. android 仿qq换肤功能,Android插件化的思考——仿QQ一键换肤,思考比实现更重要!.doc...
  9. SQL事务回滚的两种方式
  10. mysql数据库 navicat premium mac 破解教程