插入/选择时死锁

Dav*_*ldo 8 sql sql-server deadlock database-deadlocks

好吧,我完全迷失在死锁问题上。我只是不知道如何解决这个问题。

\n

我有这三个表(我删除了不重要的列):

\n
CREATE TABLE [dbo].[ManageServicesRequest]\n(\n    [ReferenceTransactionId]    INT                 NOT NULL,\n    [OrderDate]                 DATETIMEOFFSET(7)   NOT NULL,\n    [QueuePriority]             INT                 NOT NULL,\n    [Queued]                    DATETIMEOFFSET(7)   NULL,\n    CONSTRAINT [PK_ManageServicesRequest] PRIMARY KEY CLUSTERED ([ReferenceTransactionId]),\n)\n\nCREATE TABLE [dbo].[ServiceChange]\n(\n    [ReferenceTransactionId]    INT                 NOT NULL,\n    [ServiceId]                 VARCHAR(50)         NOT NULL,\n    [ServiceStatus]             CHAR(1)             NOT NULL,\n    [ValidFrom]                 DATETIMEOFFSET(7)   NOT NULL,\n    CONSTRAINT [PK_ServiceChange] PRIMARY KEY CLUSTERED ([ReferenceTransactionId],[ServiceId]),\n    CONSTRAINT [FK_ServiceChange_ManageServiceRequest] FOREIGN KEY ([ReferenceTransactionId]) REFERENCES [ManageServicesRequest]([ReferenceTransactionId]) ON DELETE CASCADE,\n    INDEX [IDX_ServiceChange_ManageServiceRequestId] ([ReferenceTransactionId]),\n    INDEX [IDX_ServiceChange_ServiceId] ([ServiceId])\n)\n\nCREATE TABLE [dbo].[ServiceChangeParameter]\n(\n    [ReferenceTransactionId]    INT                 NOT NULL,\n    [ServiceId]                 VARCHAR(50)         NOT NULL,\n    [ParamCode]                 VARCHAR(50)         NOT NULL,\n    [ParamValue]                VARCHAR(50)         NOT NULL,\n    [ParamValidFrom]            DATETIMEOFFSET(7)   NOT NULL,\n    CONSTRAINT [PK_ServiceChangeParameter] PRIMARY KEY CLUSTERED ([ReferenceTransactionId],[ServiceId],[ParamCode]),\n    CONSTRAINT [FK_ServiceChangeParameter_ServiceChange] FOREIGN KEY ([ReferenceTransactionId],[ServiceId]) REFERENCES [ServiceChange] ([ReferenceTransactionId],[ServiceId]) ON DELETE CASCADE,\n    INDEX [IDX_ServiceChangeParameter_ManageServiceRequestId] ([ReferenceTransactionId]),\n    INDEX [IDX_ServiceChangeParameter_ServiceId] ([ServiceId]),\n    INDEX [IDX_ServiceChangeParameter_ParamCode] ([ParamCode])\n)\n
Run Code Online (Sandbox Code Playgroud)\n

这两个过程:

\n
CREATE PROCEDURE [dbo].[spCreateManageServicesRequest]\n    @ReferenceTransactionId INT,\n    @OrderDate DATETIMEOFFSET,\n    @QueuePriority INT,\n    @Services ServiceChangeUdt READONLY,\n    @Parameters ServiceChangeParameterUdt READONLY\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    BEGIN TRY\n    /* VYTVO\xc5\x98 NOV\xc3\x9d REQUEST NA ZM\xc4\x9aNU SLU\xc5\xbdEB */\n\n        /*  INSERT REQUEST  */\n        INSERT INTO [dbo].[ManageServicesRequest]\n            ([ReferenceTransactionId]\n            ,[OrderDate]\n            ,[QueuePriority]\n            ,[Queued])\n        VALUES\n            (@ReferenceTransactionId\n            ,@OrderDate\n            ,@QueuePriority\n            ,NULL)\n\n        /*  INSERT SERVICES */\n        INSERT INTO [dbo].[ServiceChange]\n            ([ReferenceTransactionId]\n            ,[ServiceId]\n            ,[ServiceStatus]\n            ,[ValidFrom])\n        SELECT \n             @ReferenceTransactionId AS [ReferenceTransactionId]\n            ,[ServiceId]\n            ,[ServiceStatus]\n            ,[ValidFrom]\n        FROM @Services AS [S]\n\n        /*  INSERT PARAMS   */\n        INSERT INTO [dbo].[ServiceChangeParameter]\n            ([ReferenceTransactionId]\n            ,[ServiceId]\n            ,[ParamCode]\n            ,[ParamValue]\n            ,[ParamValidFrom])\n        SELECT \n            @ReferenceTransactionId AS [ReferenceTransactionId]\n            ,[ServiceId]\n            ,[ParamCode]\n            ,[ParamValue]\n            ,[ParamValidFrom]\n        FROM @Parameters AS [P]\n\n    END TRY\n    BEGIN CATCH\n        THROW\n    END CATCH\nEND\n\nCREATE PROCEDURE [dbo].[spGetManageServicesRequest]\n    @ReferenceTransactionId INT\nAS\nBEGIN\n    SET NOCOUNT ON;\n\n    BEGIN TRY \n        /* VRA\xc5\xa4 MANAGE SERVICES REQUEST PODLE ID */\n\n        SELECT \n            [MR].[ReferenceTransactionId], \n            [MR].[OrderDate], \n            [MR].[QueuePriority], \n            [MR].[Queued], \n            \n            [SC].[ReferenceTransactionId], \n            [SC].[ServiceId], \n            [SC].[ServiceStatus], \n            [SC].[ValidFrom],\n            \n            [SP].[ReferenceTransactionId], \n            [SP].[ServiceId], \n            [SP].[ParamCode], \n            [SP].[ParamValue], \n            [SP].[ParamValidFrom]\n\n        FROM [dbo].[ManageServicesRequest] AS [MR]\n        LEFT JOIN [dbo].[ServiceChange] AS [SC] ON [SC].[ReferenceTransactionId] = [MR].[ReferenceTransactionId]\n        LEFT JOIN [dbo].[ServiceChangeParameter] AS [SP] ON [SP].[ReferenceTransactionId] = [SC].[ReferenceTransactionId] AND [SP].[ServiceId] = [SC].[ServiceId]\n        WHERE [MR].[ReferenceTransactionId] = @ReferenceTransactionId\n\n    END TRY\n    BEGIN CATCH\n        THROW\n    END CATCH\nEND\n
Run Code Online (Sandbox Code Playgroud)\n

现在它们以这种方式使用(这是一个简化的 C# 方法,创建一条记录,然后将记录发布到微服务队列):

\n
public async Task Consume(ConsumeContext<CreateCommand> context)\n{\n    using (var sql = sqlFactory.Cip)\n    {\n        /*SAVE REQUEST TO DATABASE*/\n        sql.StartTransaction(System.Data.IsolationLevel.Serializable); <----- First transaction starts\n\n        /* Create id */\n        var transactionId = await GetNewId(context.Message.CorrelationId);\n\n        /* Create manage services request */\n        await sql.OrderingGateway.ManageServices.Create(transactionId,  context.Message.ApiRequest.OrderDate, context.Message.ApiRequest.Priority, services);\n\n        sql.Commit(); <----- First transaction ends\n        \n\n        /// .... Some other stuff ...\n\n        /* Fetch the same object you created in the first transaction */\n        Try\n        {\n            sql.StartTransaction(System.Data.IsolationLevel.Serializable);\n            \n            var request = await sql.OrderingGateway.ManageServices.Get(transactionId); <----- HERE BE THE DEADLOCK, \n\n            request.Queued = DateTimeOffset.Now;\n            await sql.OrderingGateway.ManageServices.Update(request);\n\n            ... Here is a posting to a microservice queue ...\n        \n            sql.Commit();\n        }\n        catch (Exception)\n        {\n            sql.RollBack();\n        }\n        \n        /// .... Some other stuff ....\n}\n
Run Code Online (Sandbox Code Playgroud)\n

现在我的问题是。为什么这两个程序会陷入僵局?第一个和第二个事务永远不会针对同一记录并行运行。

\n

这是死锁的详细信息:

\n
<deadlock>\n  <victim-list>\n    <victimProcess id="process1dbfa86c4e8" />\n  </victim-list>\n  <process-list>\n    <process id="process1dbfa86c4e8" taskpriority="0" logused="0" waitresource="KEY: 18:72057594046775296 (b42d8e559092)" waittime="2503" ownerId="33411557480" transactionname="user_transaction" lasttranstarted="2021-12-01T01:06:15.303" XDES="0x1ddd2df4420" lockMode="RangeS-S" schedulerid="20" kpid="23000" status="suspended" spid="55" sbid="2" ecid="0" priority="0" trancount="1" lastbatchstarted="2021-12-01T01:06:15.310" lastbatchcompleted="2021-12-01T01:06:15.300" lastattention="1900-01-01T00:00:00.300" clientapp="Core Microsoft SqlClient Data Provider" hostpid="11020" isolationlevel="serializable (4)" xactid="33411557480" currentdb="18" currentdbname="xxx" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">\n      <executionStack>\n        <frame procname="xxx.dbo.spGetManageServicesRequest" line="10" stmtstart="356" stmtend="4256" sqlhandle="0x030012001374fc02f91433019aad000001000000000000000000000000000000000000000000000000000000"></frame>\n      </executionStack>\n    </process>\n    <process id="process1dbfa1c1c28" taskpriority="0" logused="1232" waitresource="KEY: 18:72057594046971904 (ffffffffffff)" waittime="6275" ownerId="33411563398" transactionname="user_transaction" lasttranstarted="2021-12-01T01:06:16.450" XDES="0x3d4e842c420" lockMode="RangeI-N" schedulerid="31" kpid="36432" status="suspended" spid="419" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2021-12-01T01:06:16.480" lastbatchcompleted="2021-12-01T01:06:16.463" lastattention="1900-01-01T00:00:00.463" clientapp="Core Microsoft SqlClient Data Provider"  hostpid="11020" isolationlevel="serializable (4)" xactid="33411563398" currentdb="18" currentdbname="xxx" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">\n      <executionStack>\n        <frame procname="xxx.dbo.spCreateManageServicesRequest" line="40" stmtstart="2592" stmtend="3226" sqlhandle="0x03001200f01ab84aeb1433019aad000001000000000000000000000000000000000000000000000000000000"></frame>\n      </executionStack>\n    </process>\n  </process-list>\n  <resource-list>\n    <keylock hobtid="72057594046775296" dbid="18" objectname="xxx.dbo.ServiceChange" indexname="PK_ServiceChange" id="lock202ecfd0380" mode="X" associatedObjectId="72057594046775296">\n      <owner-list>\n        <owner id="process1dbfa1c1c28" mode="X" />\n      </owner-list>\n      <waiter-list>\n        <waiter id="process1dbfa86c4e8" mode="RangeS-S" requestType="wait" />\n      </waiter-list>\n    </keylock>\n    <keylock hobtid="72057594046971904" dbid="18" objectname="xxx.dbo.ServiceChangeParameter" indexname="PK_ServiceChangeParameter" id="lock27d3d371880" mode="RangeS-S" associatedObjectId="72057594046971904">\n      <owner-list>\n        <owner id="process1dbfa86c4e8" mode="RangeS-S" />\n      </owner-list>\n      <waiter-list>\n        <waiter id="process1dbfa1c1c28" mode="RangeI-N" requestType="wait" />\n      </waiter-list>\n    </keylock>\n  </resource-list>\n</deadlock>\n
Run Code Online (Sandbox Code Playgroud)\n

为什么会出现这种僵局呢?以后我该如何避免呢?

\n

编辑:\n这是获取过程的计划:https://www.brentozar.com/pastetheplan/ ?id=B1UMMhaqF

\n

另一个编辑:\n在 GSerg 评论之后,我将死锁图中的行号从 65 更改为 40,因为删除了对问题不重要的列。

\n

Mar*_*ith 4

您最好避免可序列化的隔离级别。提供可序列化保证的方式通常容易出现死锁。

如果您无法更改存储过程以使用更具针对性的锁定提示来保证在较低隔离级别下获得所需的结果,那么您可以通过确保在取出任何锁之前先取出所有锁来防止出现这种特定的死锁ServiceChange情况在ServiceChangeParameter

做到这一点的一种方法是引入一个表变量spGetManageServicesRequest并具体化结果

SELECT ...
FROM [dbo].[ManageServicesRequest] AS [MR]
  LEFT JOIN [dbo].[ServiceChange] AS [SC]  ON [SC].[ReferenceTransactionId] = [MR].[ReferenceTransactionId]
Run Code Online (Sandbox Code Playgroud)

到表变量。

然后加入反对[dbo].[ServiceChangeParameter]以获得最终结果。

表变量引入的相分离将确保SELECT语句以与插入相同的对象顺序获取锁,这样可以防止语句SELECT已经持有锁ServiceChangeParameter并等待获取锁的死锁ServiceChange(如此处的死锁图所示) )。

查看在SELECT可序列化隔离级别运行时所取出的确切锁可能会很有启发。这些可以通过扩展事件或未记录的跟踪标志 1200 来查看。

目前您的执行计划如下。

在此输入图像描述

对于以下示例数据

INSERT INTO [dbo].[ManageServicesRequest] 
VALUES (26410821, GETDATE(), 1, GETDATE()), 
       (26410822, GETDATE(), 1, GETDATE()), 
       (26410823, GETDATE(), 1, GETDATE());

INSERT INTO [dbo].[ServiceChange] 
VALUES (26410821, 'X', 'X', GETDATE()), 
       (26410822, 'X', 'X', GETDATE()), 
       (26410823, 'X', 'X', GETDATE());

INSERT INTO [dbo].[ServiceChangeParameter]  
VALUES (26410821, 'X', 'P1','P1', GETDATE()), 
       (26410823, 'X', 'P1','P1', GETDATE());
Run Code Online (Sandbox Code Playgroud)

跟踪标志输出(对于WHERE [MR].[ReferenceTransactionId] = 26410822)是

Process 51 acquiring IS lock on OBJECT: 7:1557580587:0  (class bit2000000 ref1) result: OK

Process 51 acquiring IS lock on OBJECT: 7:1509580416:0  (class bit2000000 ref1) result: OK

Process 51 acquiring IS lock on OBJECT: 7:1477580302:0  (class bit2000000 ref1) result: OK

Process 51 acquiring IS lock on PAGE: 7:1:600  (class bit2000000 ref0) result: OK

Process 51 acquiring S lock on KEY: 7:72057594044940288 (1b148afa48fb) (class bit2000000 ref0) result: OK

Process 51 acquiring IS lock on PAGE: 7:1:608  (class bit2000000 ref0) result: OK

Process 51 acquiring RangeS-S lock on KEY: 7:72057594045005824 (a69d56b089b6) (class bit2000000 ref0) result: OK

Process 51 acquiring IS lock on PAGE: 7:1:632  (class bit2000000 ref0) result: OK

Process 51 acquiring RangeS-S lock on KEY: 7:72057594045202432 (c37d1982c3c9) (class bit2000000 ref0) result: OK

Process 51 acquiring RangeS-S lock on KEY: 7:72057594045005824 (2ef5265f2b42) (class bit2000000 ref0) result: OK
Run Code Online (Sandbox Code Playgroud)

下图显示了锁定的顺序。范围锁适用于从给定键值到其下方最近的键值(按键顺序 - 因此在图像中位于其上方!)的可能值范围。

在此输入图像描述

首先调用节点 1 并锁定Sin 的行ManageServicesRequest,然后调用节点 2 并锁定RangeS-SServiceChange行中的值的键,然后使用该行中的值进行查找ServiceChangeParameter- 在这种情况下没有匹配的值谓词的行,但RangeS-S仍然会取出一个锁,覆盖从下一个最高键到前一个键的范围((26410821, 'X', 'P1') ... (26410823, 'X', 'P1')在本例中为范围)。

然后再次调用节点 2 以查看是否还有更多行。RangeS-S即使在 中的下一行没有附加锁定的情况下ServiceChange

在死锁图的情况下,被锁定的范围似乎ServiceChangeParameter是无穷大的范围(用 表示ffffffffffff) - 当它查找索引中最后一个键或超出索引中最后一个键的键值时,就会发生这种情况。

表变量的替代方法也可能是更改查询,如下所示。

SELECT ...
FROM [dbo].[ManageServicesRequest] AS [MR]
  LEFT JOIN [dbo].[ServiceChange] AS [SC]  ON [SC].[ReferenceTransactionId] = [MR].[ReferenceTransactionId]
  LEFT HASH JOIN [dbo].[ServiceChangeParameter] AS [SP] ON [SP].[ReferenceTransactionId] = [MR].[ReferenceTransactionId] AND [SP].[ServiceId] = [SC].[ServiceId]
  WHERE [MR].[ReferenceTransactionId] = @ReferenceTransactionId
Run Code Online (Sandbox Code Playgroud)

[dbo].[ServiceChangeParameter] 上的最终谓词更改为引用[MR].[ReferenceTransactionId]而不是[SC].[ReferenceTransactionId],并添加了显式哈希连接提示。

这给出了如下所示的计划,其中所有锁ServiceChange都是在哈希表构建阶段进行的,然后再进行任何操作ServiceChangeParameter- 不改变ReferenceTransactionId条件,新计划进行扫描而不是查找ServiceChangeParameter,这就是进行更改的原因(它允许优化器在 @ReferenceTransactionId 上使用隐含的相等谓词)

在此输入图像描述