SQL Server中怎么排查死锁问题

一、背景

我们在UAT环境压测的时候,遇到了如下的死锁异常。

Caused by: com.microsoft.sqlserver.jdbc.SQLServerException: Transaction (Process ID 82) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

我们立即 查看应用日志,找到报错的方法查看,发现在一个方法对同一张表进行了3种操作,先INSERT,然后SELECT,最后DELETE。也就是说在同一个事务中,对同一张表先插入,然后查询,最后根据查询结果删除。此时,我大概意识到问题所在了。但是UAT环境中,SQL Server数据库是部署在客户侧的,不太好拿死锁报告。所以我决定在本地模拟出来这个死锁问题,然后进行修复。

二、本地模拟死锁

1.业务场景简介

我们有一张userToken表,核心字段有id、loginId和token,主要用来记录用户的登录token,用来控制系统中一个用户能不能多次登录。

我们出现死锁问题的方法是登录方法,该方法在登录时会向userToken表中插入一条数据,插入成功之后回去第三方检查这个用户的状态等是否正常,因为用户数据是第三方维护的。如果检查结果是这个用户状态不可用,那么就会去删除这个用户的token数据,同时给前端返回相应的异常信息。问题就出在删除的时候,是先根据用户的loginId去查询出该用户的所有token数据,然后找出本次登录的token数据,进行删除。为什么这里有问题后面我们再详细说明。

2.在本地模拟死锁
1). 准备数据

要模拟这个死锁场景,可以在 SQL Server Management Studio (SSMS) 或者DBeaver中创建一个简单的脚本,我使用的是DBeaver也很好用。使用以下存储过程代码:

-- 1.创建一个示例 userToken 表
CREATE TABLE userToken (
    id INT IDENTITY(1,1) PRIMARY KEY,
    loginId VARCHAR(50),
    token VARCHAR(50)
);

-- 2.创建一个存储过程,以模拟登录过程
CREATE PROCEDURE sp_Login
    @loginId VARCHAR(50)
AS
BEGIN
    -- 插入一个新记录
    INSERT INTO userToken (loginId, token) VALUES (@loginId, 'token_' + CONVERT(VARCHAR(50), NEWID()));

    WAITFOR DELAY '00:00:05'; -- 模拟延迟,更容易发生死锁
    -- 选择和删除记录
    DECLARE @id INT;
    SELECT @id = id FROM userToken WHERE loginId = @loginId;
    DELETE FROM userToken WHERE id = @id;
END;

-- 3. 在第一个窗口中模拟第一个线程

DECLARE @loginId VARCHAR(50) = 'user';

BEGIN TRANSACTION;
EXEC sp_Login @loginId;
COMMIT TRANSACTION;

-- 4. 在第二个窗口中模拟第二个线程
DECLARE @loginId VARCHAR(50) = 'user';

BEGIN TRANSACTION;
EXEC sp_Login @loginId;
COMMIT TRANSACTION;

-- 5. 在两个窗口中同时运行,模拟并发登录,并观察执行结果

2).执行存储过程并观察死锁发生

按照上面的步骤创建表和存储过程,并分别在两个窗口中同时执行。可能需要执行多次才能出现死锁。如果出现下面的两种之一,就说明已经发生了死锁。

情况一:

数据库连接工具控制台出现以下错误:SQL Error [1205] [40001]: Transaction (Process ID 63) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.

情况二:

通过sqlserver自带的扩展事件[system_health]查看死锁的详细信息,执行下面的sql如果表格中有数据则已经发生了死锁。

SELECT xdr.value('@timestamp', 'datetime') AS [Date],
    xdr.query('.') AS [Event_Data]
FROM (SELECT CAST([target_data] AS XML) AS Target_Data
            FROM sys.dm_xe_session_targets AS xt
            INNER JOIN sys.dm_xe_sessions AS xs ON xs.address = xt.event_session_address
            WHERE xs.name = N'system_health'
              AND xt.target_name = N'ring_buffer'
    ) AS XML_Data
CROSS APPLY Target_Data.nodes('RingBufferTarget/event[@name="xml_deadlock_report"]') AS XEventData(xdr)
ORDER BY [Date] DESC;

如上图,已经发生死锁。

三、死锁的详细分析

1.查看死锁报告

在上面第二步中,我们通过sqlserver自带的扩展事件[system_health]先拿到了死锁报告。如下:

<event name="xml_deadlock_report" package="sqlserver" timestamp="2024-05-10T07:53:31.599Z">
    <data name="xml_report">
        <type name="xml" package="package0"/>
        <value>
            <deadlock>
                <victim-list>
                    <victimProcess id="process19f4497c108"/>
                </victim-list>
                <process-list>
                    <process id="process19f4497c108" taskpriority="0" logused="284" waitresource="KEY: 6:72057594058768384 (e8a66f387cfa)" waittime="3342" ownerId="50677" transactionname="user_transaction" lasttranstarted="2024-05-10T15:53:23.250" XDES="0x19f4c400428" lockMode="S" schedulerid="3" kpid="7120" status="suspended" spid="63" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2024-05-10T15:53:23.250" lastbatchcompleted="2024-05-10T15:51:07.110" lastattention="1900-01-01T00:00:00.110" clientapp="DBeaver 24.0.2 - SQLEditor &lt;Script-7.sql&gt;" hostname="NCSCND13691RVD0" hostpid="30508" loginname="sa" isolationlevel="read committed (2)" xactid="50677" currentdb="6" currentdbname="deadLockDatabase" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
                        <executionStack>
                            <frame procname="deadLockDatabase.dbo.sp_Login" line="11" stmtstart="590" stmtend="698" sqlhandle="0x03000600dfe61621f0cd05016cb1000001000000000000000000000000000000000000000000000000000000">
                                SELECT @id = id FROM userToken WHERE loginId = @loginI    
                            </frame>
                            <frame procname="adhoc" line="4" stmtstart="124" stmtend="166" sqlhandle="0x02000000b95c920287375badb00b99eeb827a3f3037c6bda0000000000000000000000000000000000000000">
                                unknown    
                            </frame>
                        </executionStack>
                        <inputbuf>
                            DECLARE @loginId VARCHAR(50) = 'user';

                            BEGIN TRANSACTION;
                            EXEC sp_Login @loginId;
                            COMMIT TRANSACTION;   
                        </inputbuf>
                    </process>
                    <process id="process19f4497e4e8" taskpriority="0" logused="284" waitresource="KEY: 6:72057594058768384 (11ea04af99f6)" waittime="2677" ownerId="50681" transactionname="user_transaction" lasttranstarted="2024-05-10T15:53:23.917" XDES="0x19f4ffdc428" lockMode="S" schedulerid="2" kpid="1248" status="suspended" spid="62" sbid="0" ecid="0" priority="0" trancount="1" lastbatchstarted="2024-05-10T15:53:23.913" lastbatchcompleted="2024-05-10T15:52:46.183" lastattention="1900-01-01T00:00:00.183" clientapp="DBeaver 24.0.2 - SQLEditor &lt;Script-2.sql&gt;" hostname="NCSCND13691RVD0" hostpid="30508" loginname="sa" isolationlevel="read committed (2)" xactid="50681" currentdb="6" currentdbname="deadLockDatabase" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
                        <executionStack>
                            <frame procname="deadLockDatabase.dbo.sp_Login" line="11" stmtstart="590" stmtend="698" sqlhandle="0x03000600dfe61621f0cd05016cb1000001000000000000000000000000000000000000000000000000000000">
                            SELECT @id = id FROM userToken WHERE loginId = @loginI    
                            </frame>
                            <frame procname="adhoc" line="5" stmtstart="128" stmtend="170" sqlhandle="0x020000009bc16a079a9d61241dde15013e2cc413cd9c26920000000000000000000000000000000000000000">
                                unknown    
                            </frame>
                        </executionStack>
                        <inputbuf>
                            DECLARE @loginId VARCHAR(50) = 'user';

                            BEGIN TRANSACTION;
                            EXEC sp_Login @loginId;
                            COMMIT TRANSACTION;   
                        </inputbuf>
                    </process>
                </process-list>
                <resource-list>
                    <keylock hobtid="72057594058768384" dbid="6" objectname="deadLockDatabase.dbo.userToken" indexname="PK__userToke__3213E83FCAB09E1A" id="lock19f4f504a00" mode="X" associatedObjectId="72057594058768384">
                        <owner-list>
                            <owner id="process19f4497e4e8" mode="X"/>
                        </owner-list>
                        <waiter-list>
                            <waiter id="process19f4497c108" mode="S" requestType="wait"/>
                        </waiter-list>
                    </keylock>
                    <keylock hobtid="72057594058768384" dbid="6" objectname="deadLockDatabase.dbo.userToken" indexname="PK__userToke__3213E83FCAB09E1A" id="lock19f4f509180" mode="X" associatedObjectId="72057594058768384">
                        <owner-list>
                            <owner id="process19f4497c108" mode="X"/>
                        </owner-list>
                        <waiter-list>
                            <waiter id="process19f4497e4e8" mode="S" requestType="wait"/>
                        </waiter-list>
                    </keylock>
                </resource-list>
            </deadlock>
        </value>
    </data>
</event>
2.分析死锁报告

首先,在死锁发生的过程中,我们可以通过以下sql查询当前表锁持有的锁有哪些。

--将userToken换成自己的表名
SELECT * FROM sys.dm_tran_locks WHERE resource_type = 'OBJECT' AND resource_database_id = DB_ID() AND resource_associated_entity_id = OBJECT_ID('userToken');

我们可以看到在死锁发生的过程中,userToken表上有2把IX锁(意向排他锁)。应该就是上面执行存储过程中的2条INSERT语句产生的。

接下来,我们来详细分析一下死锁报告的内容,以了解为什么会出现死锁。

a.牺牲的进程

从报告上我们可以看到<victimProcess>,牺牲的进程是 process19f4497c108,它被suspend并等待共享锁在一个关键资源上。在sqlserver中当发生死锁时,sqlserver会选择牺牲其中的一个死锁,释放它所持有的锁,从而打破死循环。

b.进程列表

通过<process-list>我们可以看到本次有两个进程参与了死锁。

process19f4497c108(被牺牲的进程)
process19f4497e4e8

两个进程都在执行 sp_Login 存储过程,该过程将新记录插入到 userToken 表中,然后根据 loginId 列选择和删除记录。从<executionStack>可以看到是在执行SELECT @id = id FROM userToken WHERE loginId = @loginId的时候阻塞了,也就是去根据loginId去查询的时候阻塞了。

这两个进程分别等待的资源是:KEY: 6:72057594058768384 (e8a66f387cfa)和KEY: 6:72057594058768384 (11ea04af99f6)。

KEY值的含义KEY表示等待的资源是一个键,也就是索引中的特定行或行范围。以KEY: 6:72057594058768384 (e8a66f387cfa)为例。6代表数据库id,72057594058768384代表被锁索引(index)的id,也就是某一个索引,(e8a66f387cfa)代表索引中内部id,也就是在该索引中具体是哪一行,可以帮我们定位到表中特定的数据行。

关于前两个,比较简单可以通过系统表查询出来。

--72057594058768384替换为死锁报告中的KEY: 6:72057594058768384 (e8a66f387cfa)的中间数字部分
select db_id() as database_id, o. name, i. name, i. type from sys. indexes i
	inner join sys.objects o on i.object_id = o.object_id
	inner join sys.partitions p on p.index_id = i.index_id and p. object_id = i. object_id
where p.partition_id = 72057594058768384

从下面的结果中可以看到和报告下面index_name一致,锁定就是主键索引

关于(e8a66f387cfa)代表索引中内部id,可以通过一个未公布的系统函数 %%lockres%% 查看得到,如下

with cte as 
(
	select %%lockres%% as resource_key, id from userToken with(index(PK__userToke__3213E83FCAB09E1A))--替换为自己的表名和死锁报告中冲突的索引
)
select * from cte where resource_key in ( '(e8a66f387cfa)', '(11ea04af99f6)');--替换为死锁报告中等待的resource_key
c.资源列表

从<resource-list>中可以看到,有两个关键的锁在userToken表上。

lock19f4f504a00:由 process19f4497e4e8 拥有,具有排他(X)锁模式
lock19f4f509180:由 process19f4497c108 拥有,具有排他(X)锁模式

死锁发生是因为每个进程都在等待共享锁在一个资源上(userToken 表的 PK__userToke__3213E83FCAB09E1A 索引),而该资源已经被另一个进程以排他锁模式拥有的。

d.死锁场景

下面是死锁报告中描述的死锁场景:

  • process19f4497c108将一条新记录插入到userToken表中,并获取了索引(PK__userToke__3213E83FCAB09E1A)的排他锁(mode='X')。
  • process19f4497e4e8将一条新记录插入到userToken表中,并获取了索引(PK__userToke__3213E83FCAB09E1A)的排他锁(mode='X')。
  • process19f4497c108 尝试根据 loginId 去查询userToken表中的数据,由于process19f4497e4e8 持有了索引的排他锁,所以process19f4497c108必须等待锁的释放。
  • process19f4497e4e8 尝试根据 loginId 去查询userToken表中的数据,由于process19f4497c108持有了索引的排他锁,所以process19f4497e4e8 必须等待锁的释放。
  • 此时,两个进程都在等待对方释放锁,结果导致死锁。
e.结论 

死锁是由于 sp_Login 存储过程的并发执行导致的,这导致了 userToken 表上的争用。每个进程在 索引上的排他锁阻止了另一个进程执行其选择和删除操作,导致死锁。因为两个进程都持有了 userToken 表的 PK__userToke__3213E83FCAB09E1A 索引的排他锁(mode='X'),每个进程都在等待另一个进程释放其锁。

要解决这个问题,我们可以优化存储过程以减少 userToken 表上的争用。

四、解决死锁问题 

有了上面对死锁报告的详细分析,我们了解到了死锁产生的原因是锁竞争。那么我们可以减少一层锁,以避免锁的竞争。修改后存储过程如下:

-- 2.创建一个存储过程,以模拟登录过程
CREATE PROCEDURE sp_Login
    @loginId VARCHAR(50)
AS
BEGIN
    -- 插入一个新记录
    INSERT INTO userToken (loginId, token) VALUES (@loginId, 'token_' + CONVERT(VARCHAR(50), NEWID()));

    -- 直接根据loginId删除记录,减少一次查询,减少一次S锁的获取
    DELETE FROM userToken WHERE loginId = @loginId;
END;

-- 3. 在第一个窗口中模拟第一个线程

DECLARE @loginId VARCHAR(50) = 'user1';

BEGIN TRANSACTION;
EXEC sp_Login @loginId;
COMMIT TRANSACTION;

-- 4. 在第二个窗口中模拟第二个线程
DECLARE @loginId VARCHAR(50) = 'user2';

BEGIN TRANSACTION;
EXEC sp_Login @loginId;
COMMIT TRANSACTION;

-- 5. 在两个窗口中同时运行,模拟并发登录,并观察执行结果

 再次多次执行上面的存储过程,没有再遇到过死锁了。

新的存储过程分析:

在这个修改后的场景中,我们可以看到,每个窗口中都执行了一个事务,该事务包括插入一条记录、删除该记录、并提交事务。

在这种情况下,死锁的可能性非常小,因为每个窗口中的事务都是自包含的,不会等待另一个窗口中的事务释放锁。

  • 当第一个窗口执行 INSERT 语句时,它会获取该索引的 X 锁,并插入一条记录。然后,它执行 DELETE 语句,删除该记录,并释放 X 锁。最后,它提交事务。
  • 同样,第二个窗口执行 INSERT 语句时,它会获取该索引的 X 锁,并插入一条记录。然后,它执行 DELETE 语句,删除该记录,并释放 X 锁。最后,它提交事务。
  • 由于每个窗口中的事务都是独立的,不会等待另一个窗口中的事务释放锁,因此死锁的可能性非常小。

通过以上步骤,成功解决这个死锁问题。 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Arduino】数字I/O的使用

目录 1、引脚工作模式 2、写入引脚digitaWrite&#xff08;&#xff09; 3、读取引脚digitalRead(pin); 4、示例 跑马灯 1、引脚工作模式 Arduino通过pinMode()设置引脚的io工作模式&#xff0c;一共有4种模式 工作模式 Mode 说明 输出模式 OUTPUT 引脚为低阻抗状态&…

如文所示:

影响 ConnectWise 的 ScreenConnect 远程桌面访问产品的严重漏洞已被广泛利用来传播勒索软件和其他类型的恶意软件。 ConnectWise 于 2 月 19 日通知客户&#xff0c;它已发布针对关键身份验证绕过缺陷和高严重性路径遍历问题的补丁。该安全漏洞当时没有 CVE 标识符。第二天&am…

【Docker与Termux】闲置旧安卓手机上的NAS无缝部署方案

最近有了新手机,旧手机也闲置了,由于之前一直在寻找平价的NAS替代方案,旧手机在抽屉躺了N天后,我决定让它重新焕发光彩,努力工作,继续拉磨。 这个时代的旧手机可以满足NAS的基本配置需求,内存、硬盘、内置电源、WIFI模块、快速接口,简直是理想中的NAS形态,但是散热、R…

程序员学CFA——经济学

经济学 需求和供给分析消费者需求分析需求的相关概念需求需求定律需求函数需求曲线消费者剩余 需求弹性需求的自身价格弹性需求的交叉价格弹性需求的收入弹性 替代效应与收入效应替代效应收入效应替代效应和收入效应的综合作用 需求定律的例外 生产商供给分析供给分析的基本概念…

计算机发展史故事【14】

大象踢踏舞 如果要把电脑50 年的历史划分为两个不同的阶段&#xff0c;那么&#xff0c;1981 年无疑是个分界线。就在那一年&#xff0c;IBM 公司推出个人电脑PC 机&#xff0c;使人类社会大步跨进个人电脑新时代。今天&#xff0c;全世界正在使用的PC 机已达到2 亿台&#xf…

python爬虫(五)之新出行汽车爬虫

python爬虫&#xff08;五&#xff09;之新出行汽车爬虫 接完最后一单&#xff0c;终于肝完了最后一个python爬虫——新出行爬虫&#xff0c;将https://www.xchuxing.com网站上所有的汽车爬虫全部抓取下来。 import requests import json import csv from lxml import etree …

【HarmonyOS】综合应用-《校园通》

概念 本文结合之前的笔记文章知识点&#xff0c;做一个综合性的小应用。 创建一个ArkTS语言的鸿蒙项目&#xff0c;搭建首页面 其界面代码如下&#xff0c;该界面使用了垂直布局&#xff0c;相对布局&#xff0c;轮播布局&#xff0c;以及图片&#xff0c;文本等组件的综合运…

ubuntu22.04服务器docker-compose方式部署ldap服务

一&#xff1a;系统版本 二&#xff1a;部署环境 节点名称 IP 部署组件及版本 配置文件路径 机器CPU 机器内存 机器存储 Ldap 10.10.10.111 self-service-password:latest phpldapadmin:latest openldap:latest openldap:/data/openldap/config phpldapadmin&#x…

初识C++ · string的使用(1)

目录 1 STL简介 2 string类 2.1 构造 析构 拷贝 2.2 size length 2.3 [ ]的使用 2.4 迭代器 2.5 Push_Back和append 3 sort的使用 1 STL简介 STL是一个标准库&#xff0c;是C标准库的一个重要部分&#xff0c;那么什么是STL&#xff1f;STL是一个模板库&#xff0c;包…

Sass终极指南:从基础到高级,打造高效、可维护的CSS代码!

在现代网页开发中&#xff0c;CSS预处理器已经成为提高样式表维护性和可扩展性的重要工具。Sass&#xff08;Syntactically Awesome Style Sheets&#xff09;是其中最受欢迎的预处理器之一。本文将全面详细地介绍Sass&#xff0c;包括其基本介绍、主要作用、使用方法及注意事项…

2024年NOC大赛创客智慧(西瓜创客)Python复赛编程真题模拟试卷包含答案

NOC复赛python模拟题 1.编写一个程序&#xff0c;提示用户输人一个矩形的长度和宽度&#xff0c;并输出其面积, 2.试计算在区间 1 到 n的所有整数中,数字x(0≤x≤9)共出现了多少次?例如在 1到11 中&#xff0c;即在 1,2,3.45,6.7,8.9,10,11 中&#xff0c;数字 1出现了 4 次.…

工程师工具箱系列(3)Arthas

文章目录 工程师工具箱系列&#xff08;3&#xff09;Arthas安装与准备Arthas插件使用场景查看某个变量值ognl方式调用Bean方法tt(TimeTunel)方式调用Bean的方法ognl调用带参数方法 资源总览 工程师工具箱系列&#xff08;3&#xff09;Arthas Java诊断利器 安装与准备 window…

大厂常见算法50题-两数相加

专栏持续更新50道算法题&#xff0c;都是大厂高频算法题&#xff0c;建议关注, 一起巧‘背’算法! 文章目录 题目解法总结 题目 解法 定义一个节点pre&#xff0c;用于初始化结果链表的头部&#xff0c;cur指向pre&#xff0c;它将在遍历过程中用于构建新的链表。初始化进位变…

微服务部署不迷茫:4大部署模式深度解析,轻松驾驭云原生时代

微服务部署最全详解(4种常见部署模式) 微服务多实例部署 每个微服务都独立部署在自己的运行环境中&#xff0c;这是微服务架构的核心思想。 一般会采用每个主机多个服务实例&#xff0c;如下所示&#xff1a; 这是一种传统的应用部署方法&#xff0c;每个服务实例在一个或多个…

腐烂的橘子BFS

题目&#xff1a; 腐烂的橘子 在给定的 m x n 网格 grid 中&#xff0c;每个单元格可以有以下三个值之一&#xff1a; 值 0 代表空单元格&#xff1b; 值 1 代表新鲜橘子&#xff1b; 值 2 代表腐烂的橘子。 每分钟&#xff0c;腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子…

FMEA再什么情况下应用——SunFMEA软件

FMEA作为一种系统性的方法&#xff0c;旨在识别和评估潜在的故障模式、其可能的影响以及相应的预防措施&#xff0c;因此&#xff0c;它的适用场景广泛且多样。今天SunFMEA软件系统和大家一起探讨什么情况下应用FMEA&#xff1f; 首先&#xff0c;在产品设计阶段&#xff0c;F…

对比分析汽车灯罩材料使用聚碳酸酯(PC)和PMMA(亚克力)的优缺点,汽车车灯的灯罩如果破损破裂破洞了要怎么修复?

对比分析汽车灯罩材料使用聚碳酸酯&#xff08;PC&#xff09;和PMMA&#xff08;亚克力&#xff09;的优缺点&#xff0c;并给出建议。 要求&#xff1a; 1. 对比分析两种材料的性能、成本、耐用性、安全性等方面的差异。 2. 给出针对不同应用场景&#xff08;如夜间照明…

通过GRE隧道实现OSPF、BGP、IS-IS的套接使用

正文共&#xff1a;999 字 9 图&#xff0c;预估阅读时间&#xff1a;1 分钟 书接上文&#xff08;专线入云场景能否配置动态路由协议&#xff1f;&#xff09;&#xff0c;我们发现通过一定的配置&#xff0c;具体就是组合使用IBGP和静态路由&#xff0c;在使用云专线接入到资…

应用层(上篇)

应用层 应用层协议原理 网络应用程序体系解构 应用程序体系结构: 由应用程序研发者设计规定了如何在各种端系统上组织该应用程序。在选择应用程序体系结构时&#xff0c;应用程序研发者很可能利用现代网络应用程序中所使用的两种主流体系结构之一:客户-服务器体系结构或对等…

快解析Tplink端口映射如何设置

Tplink作为国内知名路由器品牌&#xff0c;有着广泛的用户群体。使用快解析端口映射是实现内网服务器被外网访问必须要做的设置&#xff0c;很多对网络不懂得小白不知道该到哪里去做&#xff0c;下面我就讲解一下tplink路由器如何做端口映射。 1&#xff1a;访问路由器 &#…