34 - 记一次线上SQL死锁事故:如何避免死锁?

news2024/12/24 1:02:17

之前我参与过一个项目,在项目初期,我们是没有将读写表分离的,而是基于一个主库完成读写操作。在业务量逐渐增大的时候,我们偶尔会收到系统的异常报警信息,DBA 通知我们数据库出现了死锁异常。

按理说业务开始是比较简单的,就是新增订单、修改订单、查询订单等操作,那为什么会出现死锁呢?经过日志分析,我们发现是作为幂等性校验的一张表经常出现死锁异常。我们和 DBA 讨论之后,初步怀疑是索引导致的死锁问题。后来我们在开发环境中模拟了相关操作,果然重现了该死锁异常。

接下来我们就通过实战来重现下该业务死锁异常。首先,创建一张订单记录表,该表主要用于校验订单重复创建:

CREATE TABLE `order_record`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` int(11) DEFAULT NULL,
  `status` int(4) DEFAULT NULL,
  `create_date` datetime(0) DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_order_status`(`order_no`,`status`) USING BTREE
) ENGINE = InnoDB

为了能重现该问题,我们先将事务设置为手动提交。这里要注意一下,MySQL 数据库和 Oracle 提交事务不太一样,MySQL 数据库默认情况下是自动提交事务,我们可以通过以下命令行查看自动提交事务是否开启:

mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

下面就操作吧,先将 MySQL 数据库的事务提交设置为手动提交,通过以下命令行可以关闭自动提交事务:

mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)

订单在做幂等性校验时,先是通过订单号检查订单是否存在,如果不存在则新增订单记录。知道具体的逻辑之后,我们再来模拟创建产生死锁的运行 SQL 语句。首先,我们模拟新建两个订单,并按照以下顺序执行幂等性校验 SQL 语句(垂直方向代表执行的时间顺序):

img

此时,我们会发现两个事务已经进入死锁状态。我们可以在 information_schema 数据库中查询到具体的死锁情况,如下图所示:

img

看到这,你可能会想,为什么 SELECT 要加 for update 排他锁,而不是使用共享锁呢?试想下,如果是两个订单号一样的请求同时进来,就有可能出现幻读。也就是说,一开始事务 A 中的查询没有该订单号,后来事务 B 新增了一个该订单号的记录,此时事务 A 再新增一条该订单号记录,就会创建重复的订单记录。面对这种情况,我们可以使用锁间隙算法来防止幻读。

1、死锁是如何产生的?

行锁的具体实现算法有三种:record lock、gap lock 以及 next-key lock。record lock 是专门对索引项加锁;gap lock 是对索引项之间的间隙加锁;next-key lock 则是前面两种的组合,对索引项以其之间的间隙加锁。

只在可重复读或以上隔离级别下的特定操作才会取得 gap lock 或 next-key lock,在 Select、Update 和 Delete 时,除了基于唯一索引的查询之外,其它索引查询时都会获取 gap lock 或 next-key lock,即锁住其扫描的范围。主键索引也属于唯一索引,所以主键索引是不会使用 gap lock 或 next-key lock。

在 MySQL 中,gap lock 默认是开启的,即 innodb_locks_unsafe_for_binlog 参数值是 disable 的,且 MySQL 中默认的是 RR 事务隔离级别。

当我们执行以下查询 SQL 时,由于 order_no 列为非唯一索引,此时又是 RR 事务隔离级别,所以 SELECT 的加锁类型为 gap lock,这里的 gap 范围是 (4,+∞)。

SELECT id FROM demo.order_record where order_no = 4 for update;

执行查询 SQL 语句获取的 gap lock 并不会导致阻塞,而当我们执行以下插入 SQL 时,会在插入间隙上再次获取插入意向锁。插入意向锁其实也是一种 gap 锁,它与 gap lock 是冲突的,所以当其它事务持有该间隙的 gap lock 时,需要等待其它事务释放 gap lock 之后,才能获取到插入意向锁。

以上事务 A 和事务 B 都持有间隙 (4,+∞)的 gap 锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的 gap 锁释放,于是就造成了循环等待,导致死锁。

INSERT INTO demo.order_record(order_nostatuscreate_date) VALUES (5, 1, ‘2019-07-13 10:57:03’);

我们可以通过以下锁的兼容矩阵图,来查看锁的兼容性:

img

2、避免死锁的措施

知道了死锁问题源自哪儿,就可以找到合适的方法来避免它了。

避免死锁最直观的方法就是在两个事务相互等待时,当一个事务的等待时间超过设置的某一阈值,就对这个事务进行回滚,另一个事务就可以继续执行了。这种方法简单有效,在 InnoDB 中,参数 innodb_lock_wait_timeout 是用来设置超时时间的。

另外,我们还可以将 order_no 列设置为唯一索引列。虽然不能防止幻读,但我们可以利用它的唯一性来保证订单记录不重复创建,这种方式唯一的缺点就是当遇到重复创建订单时会抛出异常。

我们还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用 Redis 以及 ZooKeeper 来实现,运行效率比数据库更佳。

3、其它常见的 SQL 死锁问题

这里再补充一些常见的 SQL 死锁问题,以便你遇到时也能知道其原因,从而顺利解决。

我们知道死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立。所以在一些经常需要使用互斥共用一些资源,且有可能循环等待的业务场景中,要特别注意死锁问题。

接下来,我们再来了解一个出现死锁的场景。

我们讲过,InnoDB 存储引擎的主键索引为聚簇索引,其它索引为辅助索引。如果使用辅助索引来更新数据库,就需要使用聚簇索引来更新数据库字段。如果两个更新事务使用了不同的辅助索引,或一个使用了辅助索引,一个使用了聚簇索引,就都有可能导致锁资源的循环等待。由于本身两个事务是互斥,也就构成了以上死锁的四个必要条件了。

我们还是以上面的这个订单记录表来重现下聚簇索引和辅助索引更新时,循环等待锁资源导致的死锁问题:

img

出现死锁的步骤:

img

综上可知,在更新操作时,我们应该尽量使用主键来更新表字段,这样可以有效避免一些不必要的死锁发生。

4、总结

数据库发生死锁的概率并不是很大,一旦遇到了,就一定要彻查具体原因,尽快找出解决方案,老实说,过程不简单。我们只有先对 MySQL 的 InnoDB 存储引擎有足够的了解,才能剖析出造成死锁的具体原因。

例如,以上我例举的两种发生死锁的场景,一个考验的是我们对锁算法的了解,另外一个考验则是我们对聚簇索引和辅助索引的熟悉程度。

解决死锁的最佳方式当然就是预防死锁的发生了,我们平时编程中,可以通过以下一些常规手段来预防死锁的发生:

  1. 在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁;

  2. 在允许幻读和不可重复读的情况下,尽量使用 RC 事务隔离级别,可以避免 gap lock 导致的死锁问题;

  3. 更新表时,尽量使用主键更新;

  4. 避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率;

  5. 设置锁等待超时参数,我们可以通过 innodb_lock_wait_timeout 设置合理的等待超时阈值,特别是在一些高并发的业务中,我们可以尽量将该值设置得小一些,避免大量事务等待,占用系统资源,造成严重的性能开销。

5、 思考题

除了设置 innodb_lock_wait_timeout 参数来避免已经产生死锁的 SQL 长时间等待,你还知道其它方法来解决类似问题吗?

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

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

相关文章

好用的json处理工具He3 JSON

官网地址:https://he3app.com/zh/ json格式化 https://portal.he3app.com/home/extension/json-to-pretty 其他 https://portal.he3app.com/home/category

笔记-PC端wireshark采集FPGA数据的操作

wireshark采集FPGA的数据 目录 一、准备工作二、操作步骤 一、准备工作 1、软件:wireshark 2、平台:PC(本人是win11)、带有以太网功能的zynq平台 3、网线: 用网线连接zynq板子和PC的以太口端口 二、操作步骤 1、打开任务管理器…

【算法】滑动窗口题单——1.定长滑动窗口⭐

文章目录 1456. 定长子串中元音的最大数目2269. 找到一个数字的 K 美丽值1984. 学生分数的最小差值(排序)643. 子数组最大平均数 I1343. 大小为 K 且平均值大于等于阈值的子数组数目2090. 半径为 k 的子数组平均值2379. 得到 K 个黑块的最少涂色次数1052…

【问题系列】消费者与MQ连接断开问题解决方案(一)

1. 问题描述 当使用RabbitMQ作为中间件,而消费者为服务时,可能会出现以下情况:在长时间没有消息传递后,消费者与RabbitMQ之间出现连接断开,导致无法处理新消息。解决这一问题的方法是重启Python消费者服务,…

okhttp系列-拦截器的执行顺序

1.将拦截器添加到ArrayList final class RealCall implements Call {Response getResponseWithInterceptorChain() throws IOException {//将Interceptor添加到ArrayListList<Interceptor> interceptors new ArrayList<>();interceptors.addAll(client.intercept…

Android控件全解手册 - 任意View缩放平移工具-源码

Unity3D特效百例案例项目实战源码Android-Unity实战问题汇总游戏脚本-辅助自动化Android控件全解手册再战Android系列Scratch编程案例软考全系列Unity3D学习专栏蓝桥系列ChatGPT和AIGC &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分…

数学建模-基于BL回归模型和决策树模型对早产危险因素的探究和预测

整体求解过程概述(摘要) 近年来&#xff0c;全球早产率总体呈上升趋势&#xff0c;在我国&#xff0c;早产儿以每年 20 万的数目逐年递增&#xff0c;目前早产已经成为重大的公共卫生问题之一。据研究,早产是威胁胎儿及新生儿健康的重要因素&#xff0c;可能会造成死亡或智力体…

每日一题:LeetCode-202.面试题 08.06. 汉诺塔问题

每日一题系列&#xff08;day 07&#xff09; 前言&#xff1a; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f50e…

20世纪30年代的大危机

背景 1929年9月&#xff0c;美国财政部部长安德鲁梅隆向公众保证“现在没有担心的理由&#xff0c;这一繁荣的高潮将会继续下去”。 当时流行的一首儿歌&#xff1a;“梅隆拉响汽笛&#xff0c;胡佛敲起钟&#xff0c;华尔街发出信号&#xff0c;美国往地狱里冲&#xff01;”…

水库大坝安全在线监测系统守护水利工程的坚实屏障

随着科技的发展&#xff0c;水库大坝的安全监测已经进入了一个全新的时代。过去&#xff0c;我们无法实时监测大坝的安全状况&#xff0c;只能在灾难发生后进行补救&#xff0c;现在&#xff0c;通过WX-DB1水库大坝安全在线监测系统&#xff0c;我们能够在第一时间掌握大坝的运…

【创建和排查隐藏进程和隐藏计划任务】

Window 创建隐藏进程和隐藏计划任务&#xff1a; 隐藏进程&#xff1a; 在Windows中&#xff0c;隐藏进程主要通过修改进程属性或使用第三方工具实现。以下是一个使用PowerShell脚本创建隐藏进程的示例&#xff1a; $Script {Start-Process -FilePath "notepad.exe"…

Kubernetes Pod 介绍

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容Pod 介绍与原理讲解Pod 生命周期管理Pod 的健康检查 &#x1f4e2;文章总结&#x1f4e5;博主目标 &#x1f50a;博主介绍 &#x1f31f;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51…

Peter算法小课堂—高精度减法

给大家看个小视频高精度减法_哔哩哔哩_bilibili 基本思想 计算机模拟人类做竖式计算&#xff0c;从而得到正确答案 大家还记得小学时学的“减法竖式”吗&#xff1f;是不是这样 x-y问题 函数总览&#xff1a; 1.converts() 字符串转为高精度大数 2.le() 判断大小 3.sub() …

这个蓄电池监控神技,谁用谁知道!

随着电力需求的不断增长&#xff0c;蓄电池作为能量存储的关键组件在各个领域得到了广泛应用&#xff0c;为了确保蓄电池的可靠性和性能&#xff0c;监控系统变得至关重要。 蓄电池监控系统可以实时监测电池的状态、健康状况以及充放电过程&#xff0c;从而提高电池的寿命、降低…

比尔盖茨:GPT-5不会比GPT-4好多少,生成式AI已达到极限

比尔盖茨一句爆料&#xff0c;成为机器学习社区热议焦点&#xff1a; “GPT-5不会比GPT-4好多少。” 虽然他已不再正式参与微软的日常运营&#xff0c;但仍在担任顾问&#xff0c;并且熟悉OpenAI领导团队的想法。 消息来自德国《商报》&#xff08;Handelsblatt&#xff09;对…

麒麟操作系统光盘救援模式

麒麟操作系统光盘救援模式 Kylin V4 桌面版&#xff1a; 启动主机后&#xff0c;插入系统光盘&#xff0c;在 BIOS 启动项里设置成从光盘启动后保存退出重启主机。 稍等片刻就会到启动菜单选项&#xff0c;到启动菜单界面后选择第一项试用银河麒麟操作系统而不安 装&#xff…

酷开系统 | 追求娱乐不止一种方式,酷开科技带你开启新体验!

在当今社会&#xff0c;娱乐方式多种多样&#xff0c;人们对于娱乐的需求和追求也在日益增长。然而&#xff0c;传统的娱乐方式已经无法满足大家对于多元化、个性化的体验需求。此时&#xff0c;酷开科技以其独特的视角和领先的技术&#xff0c;为消费者们带来了全新的娱乐体验…

DockerCompose修改某个服务的配置(添加或编辑端口号映射)后如何重启单个服务使其生效

场景 docker-compose入门以及部署SpringBootVueRedisMysql(前后端分离项目)以若依前后端分离版为例&#xff1a; docker-compose入门以及部署SpringBootVueRedisMysql(前后端分离项目)以若依前后端分离版为例_docker-compose部署java mysql redis-CSDN博客 上面讲了docker c…

数据安全建设的六大关键步骤

随着数字化时代的到来&#xff0c;数据安全已经成为企业和社会组织必须面对的重要问题。数据泄露、网络攻击等安全事件频发&#xff0c;给个人隐私、企业利益和国家安全带来了严重威胁。因此&#xff0c;加强数据安全建设已成为刻不容缓的任务。以下是数据安全建设的六大关键步…

解决Maven项目jar包下载失败的问题

文章目录 配置国内的Maven源引入正确的settings.xml文件重新下载jar包对后面要创建的新项目也统一配置仍然失败的解决办法 配置国内的Maven源 引入正确的settings.xml文件 如果该目录下的 settings.xml文件不存在或者错误&#xff0c;要创建一个 settings.xml文件并写入正确的…