SQL Server:在没有事务的存储过程上出现死锁受害者错误

Posted

技术标签:

【中文标题】SQL Server:在没有事务的存储过程上出现死锁受害者错误【英文标题】:SQL Server: getting deadlock victim error on a stored procedure without transaction 【发布时间】:2018-02-18 09:05:08 【问题描述】:

我有一张名为Storage 的桌子,采用这种设计:

CREATE TABLE [dbo].[Storage]
(
    [Id] [INT] IDENTITY(1,1) NOT NULL,
    [GameId] [INT] NOT NULL,
    [UserId] [INT] NOT NULL,
    [Status] [TINYINT] NOT NULL,
    [CreatedAt] [DATETIME] NOT NULL,
    [UpdatedAt] [DATETIME] NULL,
    [Data] [NVARCHAR](MAX) NOT NULL,

    CONSTRAINT [PK_Storage] 
        PRIMARY KEY CLUSTERED ([Id] ASC)
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

我在存储过程MMO_Storage_Set 中收到“死锁受害者错误”:

CREATE PROCEDURE [dbo].[MMO_Storage_Set] 
    @Data NVARCHAR(MAX),
    @GameKey INT,
    @UserId INT,
    @ErrorCode INT OUT 
AS
BEGIN
    SET NOCOUNT ON;

    -- user must have 1 active session at minimum
    IF NOT EXISTS(SELECT Id FROM dbo.[Session] WITH(NOLOCK) 
                  WHERE UserId = @UserId AND ([Status] = 1))
    BEGIN
        SET @ErrorCode = -3
        RETURN
    END

    DECLARE @GameId INT = NULL
    DECLARE @GameStatus TINYINT = NULL

    SELECT @GameStatus = [Status], @GameId = Id
    FROM dbo.[Game] WITH(NOLOCK)
    WHERE ([Key] = @GameKey)

    -- Game not found
    IF @GameStatus IS NULL
    BEGIN
        SET @ErrorCode = -5
        RETURN
    END

    -- Game is not valid
    IF @GameStatus != 1
    BEGIN
        SET @ErrorCode = -6
        RETURN
    END

    SET @ErrorCode = 0

    IF (NOT EXISTS (SELECT ID FROM [Storage] WITH (NOLOCK) 
                    WHERE [UserID] = @UserId AND [GameId] = @GameId))
    BEGIN
        INSERT INTO dbo.Storage (GameId, UserId,[Status], CreatedAt, UpdatedAt, Data)
        VALUES (@GameId, @UserId, 1, GETDATE(), NULL, @Data)
    END
    ELSE
    BEGIN       
        UPDATE dbo.Storage 
        SET Data = @Data, UpdatedAt = GETDATE()
        WHERE (GameID = @GameId) AND (UserId = @UserId) AND ([Status] = 1)
    END

    SET @ErrorCode = 1
END

我的错误是:

事务(进程 ID 55)因锁定而死锁 |与另一个进程通信缓冲区资源,并已被选为死锁牺牲品。重新运行事务。

我使用 SQL Server Profiler 来跟踪死锁,您可以在 SQL Server Profiler 生成的死锁报告中看到:

<deadlock>
 <victim-list>
  <victimProcess id="process10754aca8" />
 </victim-list>
 <process-list>
  <process id="process10754aca8" taskpriority="0" logused="0" waitresource="PAGE: 9:1:1167 " waittime="1302" ownerId="501754107" transactionname="UPDATE" lasttranstarted="2018-02-18T02:21:16.990" XDES="0x270741590" lockMode="U" schedulerid="3" kpid="6700" status="suspended" spid="63" sbid="0" ecid="1" priority="0" trancount="0" lastbatchstarted="2018-02-18T02:21:16.810" lastbatchcompleted="2018-02-18T02:21:16.817" lastattention="1900-01-01T00:00:00.817" clientapp=".Net SqlClient Data Provider" hostname="APP-SOCCER-VAS" hostpid="11332" isolationlevel="read committed (2)" xactid="501754107" currentdb="9" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
   <executionStack>
    <frame procname="SoccerChampion.dbo.MMO_Storage_Set" line="60" stmtstart="2870" stmtend="3132" sqlhandle="0x030009008b0bd114d1f2a4004da8000001000000000000000000000000000000000000000000000000000000">
Update dbo.Storage 
        Set Data = @Data, UpdatedAt = GETDATE()
        WHERE (GameID = @GameId) AND (UserId = @UserId) AND ([Status] = 1    </frame>
   </executionStack>
   <inputbuf>
Proc [Database Id = 9 Object Id = 349244299]   </inputbuf>
  </process>
  <process id="process136d8d088" taskpriority="0" logused="5384" waitresource="PAGE: 9:1:1167 " waittime="1056" ownerId="501753989" transactionname="UPDATE" lasttranstarted="2018-02-18T02:21:16.773" XDES="0x1767f9ce0" lockMode="U" schedulerid="1" kpid="9920" status="suspended" spid="52" sbid="0" ecid="3" priority="0" trancount="0" lastbatchstarted="2018-02-18T02:21:16.590" lastbatchcompleted="2018-02-18T02:21:16.597" lastattention="1900-01-01T00:00:00.597" clientapp=".Net SqlClient Data Provider" hostname="APP-SOCCER-VAS" hostpid="11332" isolationlevel="read committed (2)" xactid="501753989" currentdb="9" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
   <executionStack>
    <frame procname="SoccerChampion.dbo.MMO_Storage_Set" line="60" stmtstart="2870" stmtend="3132" sqlhandle="0x030009008b0bd114d1f2a4004da8000001000000000000000000000000000000000000000000000000000000">
Update dbo.Storage 
        Set Data = @Data, UpdatedAt = GETDATE()
        WHERE (GameID = @GameId) AND (UserId = @UserId) AND ([Status] = 1    </frame>
   </executionStack>
   <inputbuf>
Proc [Database Id = 9 Object Id = 349244299]   </inputbuf>
  </process>
  <process id="process1001b7848" taskpriority="0" logused="10000" waittime="654" schedulerid="4" kpid="10028" status="suspended" spid="52" sbid="0" ecid="0" priority="0" trancount="2" lastbatchstarted="2018-02-18T02:21:16.590" lastbatchcompleted="2018-02-18T02:21:16.597" lastattention="1900-01-01T00:00:00.597" clientapp=".Net SqlClient Data Provider" hostname="APP-SOCCER-VAS" hostpid="11332" loginname="SC_Core" isolationlevel="read committed (2)" xactid="501753989" currentdb="9" lockTimeout="4294967295" clientoption1="673185824" clientoption2="128056">
   <executionStack>
    <frame procname="SoccerChampion.dbo.MMO_Storage_Set" line="60" stmtstart="2870" stmtend="3132" sqlhandle="0x030009008b0bd114d1f2a4004da8000001000000000000000000000000000000000000000000000000000000">
Update dbo.Storage 
        Set Data = @Data, UpdatedAt = GETDATE()
        WHERE (GameID = @GameId) AND (UserId = @UserId) AND ([Status] = 1    </frame>
   </executionStack>
   <inputbuf>
Proc [Database Id = 9 Object Id = 349244299]   </inputbuf>
  </process>
 </process-list>
 <resource-list>
  <pagelock fileid="1" pageid="1167" dbid="9" subresource="FULL" objectname="SoccerChampion.dbo.Storage" id="lock205c8bb00" mode="U" associatedObjectId="72057594084524032">
   <owner-list>
    <owner id="process1001b7848" mode="U" />
   </owner-list>
   <waiter-list>
    <waiter id="process10754aca8" mode="U" requestType="wait" />
   </waiter-list>
  </pagelock>
  <pagelock fileid="1" pageid="1167" dbid="9" subresource="FULL" objectname="SoccerChampion.dbo.Storage" id="lock205c8bb00" mode="U" associatedObjectId="72057594084524032">
   <owner-list>
    <owner id="process10754aca8" mode="U" requestType="wait" />
   </owner-list>
   <waiter-list>
    <waiter id="process136d8d088" mode="U" requestType="wait" />
   </waiter-list>
  </pagelock>
  <exchangeEvent id="Pipeb1ff6380" WaitType="e_waitPipeGetRow" nodeId="2">
   <owner-list>
    <owner id="process136d8d088" />
   </owner-list>
   <waiter-list>
    <waiter id="process1001b7848" />
   </waiter-list>
  </exchangeEvent>
 </resource-list>
</deadlock>

我想知道为什么我在没有事务的 update 查询和没有锁的读取查询中遇到死锁!!

谁能告诉我为什么会收到此错误以及如何解决?

【问题讨论】:

MMO_Storage_Set != SAPI_Storage_Set @IvanStarostin 谢谢,我解决了这个问题 我打赌缺少索引(GameID, UserId) 会导致扫描并且每次更新都需要锁定整个表。 @IvanStarostin 让我试试 UPDATE 语句 always 在事务中运行!如果您自己没有显式指定您,那么就会使用一个隐式事务 【参考方案1】:

不是锁的答案,但我在这里看到了问题

IF (NOT EXISTS (SELECT ID FROM [Storage] WITH (NOLOCK) 
                WHERE [UserID] = @UserId AND [GameId] = @GameId))  

但如果确实存在,则更新

AND ([Status] = 1) 

您没有检查状态 = 1,因此记录可能不存在

搜索使用 merge 的 upsert。你可以消除那个IF (NOT EXISTS。这不是清理代码类型的建议。它可能会解决您的锁定问题。肯定不会痛的。

也许在更新时采取明确的行锁,但表提示应该是最后的努力。

【讨论】:

以上是关于SQL Server:在没有事务的存储过程上出现死锁受害者错误的主要内容,如果未能解决你的问题,请参考以下文章

在一个事务中调用多个 SQL Server 存储过程

sql server存储过程回滚事务

为啥我的存储过程没有出现在我的 SQL Server 数据库中?

可以在 SQL Server 中没有相应的 BEGIN TRAN 的情况下提交事务吗?

SQL SERVER存储过程中使用事务

SQL Server 2005 事务复制无法发布包含索引创建的存储过程