头图

Hello,大家好,我是 Skow

阅读这篇文章之前,大家可以问问自己

  • 何为死锁?
  • Mysql具有哪些锁?
  • Mysql 的锁模式兼容矩阵你是否清楚?
  • 如何排查死锁问题?

如果你可以闭着眼睛回答出来这些问题的,那么就默默点赞离开👍🏻

如果你对上面的知识点,还有点含糊不清,那么这篇文章将会带你从一个真实业务场景入手,分析死锁问题,希望本文对你有所帮助,Let's go 🤨

图片

业务背景

目前我司有两个系统 A 系统、B系统

A 系统存放着公司所有人员的信息

B 系统需要日终定时从 A 系统同步数据

人员已在 B 系统中存在,则更新,不存在则插入

因人员信息过多,所以采取多线程方式同步人员数据

在验证代码的时候,😡测试人员怒气冲冲的反应, sync\_user 也就是我们的同步人员的那张数据表打不开了

遇事莫慌,先甩锅运维,“小姐姐,莫急莫慌,肯定是数据库系统出问题了"

经过运维和DBA的排查,其实罪魁祸首是开发

我们的代码导致了这张表出现了死锁,从而导致表打不开了

那,到底是为何发生了死锁?接下来我们还原一下案发现场

案发还原

看一下原始的建表语句(当然不会给你看真实的表)

CREATE TABLE `sync_user` (
`user_id` VARCHAR ( 32 ) NOT NULL COMMENT '用户 ID',
`user_name` VARCHAR ( 32 ) DEFAULT NULL COMMENT '用户姓名',
`login_account` VARCHAR ( 50 ) DEFAULT NULL COMMENT '登陆账号',
PRIMARY KEY ( `user_id` ),
KEY `idx_login_account` ( `login_account` ) USING BTREE 
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = '用户信息表';

现在系统中有张三、李四两个用户

图片

表已经准备就绪了,接下来看下我们的数据隔离级别、并且把我们的自动提交关闭

图片

图片

正戏开始!!!✍️

按照下图模拟下我们并发同步数据的情况

  • 开启 事务1,执行更新语句 >UPDATE sync\_user SET user\_name = "张三2" where login\_account = "zhangsan";
  • 开启 事务2,执行更新语句 > UPDATE sync\_user SET user\_name = "李四2" where login\_account = "lisi"; 更新成功
  • 回到事务1,执行插入语句 > INSERT INTO sync\_user (user\_id, user\_name, login\_account) VALUES ('3', '王五', 'wangwu');-- 此条语句阻塞中
  • 回到事务2,执行插入语句 > INSERT INTO sync\_user (user\_id, user\_name, login\_account) VALUES ('4', '杨六', 'yangliu'); -- 出现死锁,并且事务1 的插入语句执行成功

图片

以上就是我们模拟的并发情况,课代表总结图如下 👇

图片

死锁分析

通过事务2 提示的 Deadlock found when trying to get lock; try restarting transaction

我们可以很明白的得到,这就是发生了死锁情况

那么,什么是死锁呢?

大学老师都是这样告诉我们的:死锁是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进 造成死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个进程使用;
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
  • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系;

ok,什么是死锁和造成死锁的必要条件我们已经知道了,要产生死锁,必先有锁,那么 Mysql 有哪些锁呢?

Mysql按照锁模式区分有:记录锁、gap锁、next-key锁、插入意向锁

具体的锁作用篇幅限制,就不展开说明

锁的兼容矩阵为:横行为当前已经持有的锁,纵向为正在请求的锁

图片

接下来,正式分析一下事务1、事务2各自拿到了什么锁

  • 事务1在更新zhangsan 张三的时候
  • 间隙锁:UPDATE 语句会在非唯一索引的 login\_account 加上间隙锁,即获得 (lisi,zhangsan)、(zhangsan,+∞)
  • 记录锁:因为 login\_account 为索引,会在 zhangsan 这一行加锁
  • Next-Key锁:Next-Key锁 = 记录锁 + 间隙锁,所以该 UPDATE 语句就有了 (lisi,zhangsan] 的 Next-Key锁
  • 综上所述:更新张三的语句获得了
  • Next-Key 锁-> (lisi,zhangsan]
  • Gap锁 -> (zhangsan,+∞)
  • 事务1在插入 wangwu 王五的时候
  • 间隙锁:因为 wangwu(在lisi和zhangsan之间),所以需要请求加 (lisi,zhangsan) 的间隙锁
  • 插入意向锁(Insert Intention):插入意向锁是在插入一行记录操作之前设置的一种间隙锁,这个锁释放了一种插入方式的信号,即事务A需要插入意向锁 (lisi,zhangsan)

因此,事务1的 UPDATE 语句和 INSERT 语句执行完,它是持有了 (lisi,zhangsan] 的 Next-Key锁,(zhangsan,+∞) 的Gap锁,想拿到  (lisi,zhangsan) 的插入意向排它锁

事务2的分析也如上举例,我们直接给出答案

事务2的 UPDATE 语句和 INSERT 语句执行完,它是持有了 (-∞,lisi] 的 Next-Key锁,(lisi,zhangsan) 的Gap锁,想拿到  (lisi,zhangsan) 的插入意向排它锁

锁已经分析完毕了,接下来,我们需要去查看一下事务的日志结果

图片

真相即将浮出水面

图片

事务1期望拿到 (lisi,zhangsan) 的插入意向锁,但是这个范围当前被事务2的 (lisi,zhangsan] 的 gap 锁占有了,这两把锁又是冲突的

事务2期望拿到 (lisi,zhangsan) 的插入意向锁,但是这个范围被事务1的 (lisi,zhangsan] 的 Next-Key 锁占有了,这两把锁又是冲突的

所以死锁发生。因为Innodb的底层机制,它会让其中一个事务让出资源,另外的事务执行成功,这就是为什么你最后看到事务1插入成功了,但是事务2的插入显示了Deadlock found

总结

死锁原因已经分析出来了,那我们以后面对死锁,整体解决思路是什么呢?

  • 甩锅运维
  • 模拟死锁场景
  • show engine innodb status;查看死锁日志
  • 找出死锁SQL
  • SQL加锁分析,这个可以去官网看哈
  • 分析死锁日志(持有什么锁,等待什么锁)
  • 熟悉锁模式兼容矩阵,InnoDB存储引擎中锁的兼容性矩阵。

参考文章:

丁奇 《MySql 实战45讲》

捡田螺的小男孩 《手把手教你分析Mysql死锁问题》


文章结束 🤣

如果本文对你有所帮助的话,那就点个赞吧

更多分享尽在微信公众号【codeLiveHouse】

公众号回复 “资料” 可以获取大厂面试题/技术文档/电子书等等


Issues
10 声望1 粉丝

热爱音乐,享受生活,希望分享可以帮助到有需要的人