更新查询在同一张表上的 Sql 查询死锁

Posted

技术标签:

【中文标题】更新查询在同一张表上的 Sql 查询死锁【英文标题】:Sql query deadlock on same table with update query 【发布时间】:2017-12-27 12:48:51 【问题描述】:

下面的存储过程出现死锁错误 - UpdateTestEvents。

下面是xml死锁报告:

<deadlock>
 <victim-list>
  <victimProcess id="process1128b529468" />
 </victim-list>
 <process-list>
  <process id="process1128b529468" taskpriority="0" logused="0" waitresource="KEY: 7:72057594042777600 (fec90e3a2350)" waittime="2364" ownerId="158290173" transactionname="user_transaction" lasttranstarted="2017-12-17T01:20:45.553" XDES="0x1064ff98408" lockMode="U" schedulerid="9" kpid="6664" status="suspended" spid="57" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2017-12-17T01:20:45.547" lastbatchcompleted="2017-12-17T01:20:45.543" lastattention="1900-01-01T00:00:00.543" clientapp="EntityFramework" hostname="STAAP8895" hostpid="3616" loginname="XLAPSDBScoring" isolationlevel="read committed (2)" xactid="158290173" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
   <executionStack>
    <frame procname="analytics.dbo.UpdateTestEvents" line="25" stmtstart="1836" stmtend="2132" sqlhandle="0x030007005e4b4b2a97304c0155a5000001000000000000000000000000000000000000000000000000000000">
UPDATE dbo.History
SET Ignore = 0
WHERE Number = @Number
    AND dbo.StringsMatch(@candidate, ACType, DEFAULT) =    </frame>
   </executionStack>
   <inputbuf>
Proc [Database Id = 7 Object Id = 709577566]   </inputbuf>
  </process>
  <process id="process1127e522ca8" taskpriority="0" logused="301092" waitresource="KEY: 7:72057594043039744 (c41e1b4226b6)" waittime="2364" ownerId="158290165" transactionname="user_transaction" lasttranstarted="2017-12-17T01:20:45.447" XDES="0xf8dc5ff8a8" lockMode="U" schedulerid="2" kpid="4888" status="suspended" spid="60" sbid="2" ecid="0" priority="0" trancount="2" lastbatchstarted="2017-12-17T01:20:45.440" lastbatchcompleted="2017-12-17T01:20:45.437" lastattention="1900-01-01T00:00:00.437" clientapp="EntityFramework" hostname="STAAP1493" hostpid="3304" loginname="XLAPSDBScoring" isolationlevel="read committed (2)" xactid="158290165" currentdb="7" lockTimeout="4294967295" clientoption1="671088672" clientoption2="128056">
   <executionStack>
    <frame procname="analytics.dbo.UpdateTestEvents" line="32" stmtstart="2370" stmtend="3926" sqlhandle="0x030007005e4b4b2a97304c0155a5000001000000000000000000000000000000000000000000000000000000">
WITH ValidOriginsAndDestinations AS
(
      SELECT Origin FROM dbo.History
     WHERE Ignore = 0
        AND Number = @Number
     UNION ALL
         SELECT Destination FROM dbo.History
     WHERE Ignore = 0
        AND Number = @Number
)
UPDATE fh
SET Ignore = 0
FROM dbo.History AS fh
WHERE Number = @Number
    AND Ignore = 1
    AND 
    (       
        Origin IN (SELECT * FROM ValidOriginsAndDestinations)
        OR Destination IN (SELECT * FROM ValidOriginsAndDestinations)    </frame>
   </executionStack>
   <inputbuf>
Proc [Database Id = 7 Object Id = 709577566]   </inputbuf>
  </process>
 </process-list>
 <resource-list>
  <keylock hobtid="72057594042777600" dbid="7" objectname="analytics.dbo.History" indexname="PK_History" id="lockf50c41ac80" mode="X" associatedObjectId="72057594042777600">
   <owner-list>
    <owner id="process1127e522ca8" mode="X" />
   </owner-list>
   <waiter-list>
    <waiter id="process1128b529468" mode="U" requestType="wait" />
   </waiter-list>
  </keylock>
  <keylock hobtid="72057594043039744" dbid="7" objectname="xl_analytics_aviation.dbo.History" indexname="IX_History_Number" id="lock1128b5be680" mode="U" associatedObjectId="72057594043039744">
   <owner-list>
    <owner id="process1128b529468" mode="U" />
   </owner-list>
   <waiter-list>
    <waiter id="process1127e522ca8" mode="U" requestType="wait" />
   </waiter-list>
  </keylock>
 </resource-list>
</deadlock>

存储过程如下所示:

CREATE PROCEDURE [dbo].[UpdateTestEvents]
    @Number varchar(50)
AS
DECLARE @tolerance decimal(10,10) = 0.15
DECLARE @totalEvents decimal(10,0) = (SELECT COUNT(*) FROM dbo.History fh WHERE fh.Number = @Number)

IF(@totalEvents = 0) RETURN

DECLARE @candidate VARCHAR(50) = 
    (SELECT TOP 1 ACType
    FROM dbo.History AS fh
    WHERE fh.Number = @Number
    GROUP BY ACType
    HAVING (COUNT(*) / @totalEvents) > @tolerance
    ORDER BY MAX(ActualDepartureTime) DESC)
SELECT @candidate

UPDATE dbo.History
SET Ignore = 0
WHERE Number = @Number
    AND dbo.StringsMatch(@candidate, ACType, DEFAULT) = 1;


WITH ValidOriginsAndDestinations AS
(

    SELECT Origin FROM dbo.History
     WHERE Ignore = 0
        AND Number = @Number
     UNION ALL
     SELECT Destination FROM dbo.History
     WHERE Ignore = 0
        AND Number = @Number
)

UPDATE fh
SET Ignore = 0
FROM dbo.History AS fh
WHERE Number = @Number
    AND Ignore = 1
    AND 
    (
        Origin IN (SELECT * FROM ValidOriginsAndDestinations)
        OR Destination IN (SELECT * FROM ValidOriginsAndDestinations)
    );

WITH Comfirmeddt AS
(
    SELECT a.lat, a.long FROM dbo.places AS a
    JOIN dbo.History AS fh
    ON     a.tidentifier = fh.Origin
        OR a.tidentifier = fh.Destination    
    WHERE fh.Number = @Number
    GROUP BY a.tidentifier, a.lat, a.long
)
UPDATE fh
SET Ignore = 0
FROM dbo.History AS fh
JOIN dbo.places AS a
ON a.tidentifier = fh.Origin
    OR a.tidentifier = fh.Destination
WHERE fh.Ignore = 1
AND fh.Number = @Number
AND EXISTS
(
    SELECT * FROM Comfirmeddt AS confirmed   
    WHERE
    (
        a.lat  < confirmed.lat  + 0.5 AND a.lat  > confirmed.lat  - 0.5 AND
        a.long < confirmed.long + 0.5 AND a.long > confirmed.long - 0.5
    )
)

GO

我收到以下错误: 执行命令时发生错误。有关详细信息,请参阅内部异常。事务(进程 ID 57)与另一个进程在锁资源上死锁,并已被选为死锁牺牲品。重新运行事务

[PK_History]的索引定义如下:

ALTER TABLE [dbo].[History] ADD  CONSTRAINT [PK_History] PRIMARY KEY CLUSTERED 
(
    [HashCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

主键是HashCode 有人可以建议我在这个查询上可以做些什么来避免将来出现这种死锁。

请在下面找到表结构:

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[History](
    [HashCode] [varchar](50) NOT NULL,
    [FaID] [varchar](50) NOT NULL,
    [Number] [varchar](255) NOT NULL,
    [ActualArrivalTime] [datetime] NULL,
    [ActualDepartureTime] [datetime] NULL,
    [ACType] [varchar](10) NULL,
    [Destination] [varchar](40) NULL,
    [DestinationCity] [varchar](100) NULL,  
    [Origin] [varchar](40) NULL,
    [Ignore] [bit] NOT NULL DEFAULT ((1)),
    [FlNumber] [varchar](255) NULL,
    [DateAdded] [datetime] NOT NULL DEFAULT (getdate()),
 CONSTRAINT [History] PRIMARY KEY CLUSTERED 
(
    [HashCode] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

SET ANSI_PADDING ON
GO

ALTER TABLE [dbo].[History]  WITH CHECK ADD  CONSTRAINT [FK_Number] FOREIGN KEY([Number])
REFERENCES [dbo].[Number] ([Number])
GO

ALTER TABLE [dbo].[History] CHECK CONSTRAINT [FK_Number]
GO

对存储过程的调用发生在 .NET 代码中,由实体框架实现。下面是从应用程序调用这个存储过程的框架

using (var db = new NumberDbContext())
            
foreach (var tn in Numbers)
            
 db.UpdateTestEvents(tailNumber);


请在下面找到查询的执行计划: https://www.brentozar.com/pastetheplan/?id=B1dGzUMQf

如果 IX_History_Number 索引也定义:

CREATE NONCLUSTERED INDEX [IX_History_Number] ON [dbo].[History]
(
    [Number] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

【问题讨论】:

PK_Histor 中的主键字段是什么,IX_History_Number 定义是什么? IX_History_Number 是为名为 Number 的列之一创建的非聚集非唯一索引。并且 PK_History 是表的聚集主键索引 >>>而 PK_History 是表的聚集主键索引 @user2081126,不同的索引访问路径导致了死锁。避免这些死锁的一种快速而简单的方法是在@number 值上使用独占事务级别sp_getapplock 来序列化访问。我假设 proc 是从应用代码中发起的事务调用的。 @sepupic:我将用索引的定义更新问题。主键是 HashCode,索引如下所示: ALTER TABLE [dbo].[History] ​​ADD CONSTRAINT [PK_History] ​​PRIMARY KEY CLUSTERED ( [HashCode] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF,ONLINE = OFF,ALLOW_ROW_LOCKS = ON,ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO 【参考方案1】:

死锁是由于查询使用不同的索引来访问相同的行。避免这种死锁情况的一种方法是使用application lock 序列化对相同@Number 值的访问,直到事务被提交。请注意,必须从具有 Transaction 锁所有者的显式事务调用 proc。

CREATE PROCEDURE [dbo].[UpdateTestEvents]
    @Number varchar(50)
AS
DECLARE @return_code int;
EXEC @return_code = sp_getapplock @Resource = @Number, @LockMode = 'Exclusive', @LockOwner = 'Transaction';
IF @return_code NOT IN(0,1)
BEGIN
    RAISERROR('Unexpected sp_getapplock return code is %d',16,1,@return_code);
    RETURN @return_code;
END;
--remainder of proc code here
GO

【讨论】:

这只是检查资源是否被锁定然后抛出异常 @除非您指定@LockTimeout,否则 sp_getapplock 将阻塞,直到获得锁为止(假设默认为 -1 LOCK_TIMEOUT 无超时)。这就是正常的行锁定行为会出现的行为。 谢谢丹。但我实际上正在寻找可以在没有锁定的情况下继续执行的东西。我可以在存储的 proc/index 中修改的任何内容。您提供的建议仍然有一个异常块,当它返回除 0 或 1 之外的任何值时会被命中 如果您查看文档中除 0 或 1 以外的返回码,则此处不应出现这些情况。返回码检查是最佳实践,这就是我包含它的原因。 proc 运行多长时间以及多久执行一次?为了避免死锁,你实际上需要一把锁而不是试图避免一个。这只是 SQL 是否为您在同一索引上使用行锁定的问题,在不了解应用程序和其他工作负载的情况下,在这里提出建议可能具有挑战性。 每天执行一次,不同号码摩擦30分钟左右。【参考方案2】:

我会先检查数据库中的隔离级别,如果开启快照隔离,则数据库中获得共享锁的可能性较小。

select name
        , s.snapshot_isolation_state
        , snapshot_isolation_state_desc
        , is_read_committed_snapshot_on
        , recovery_model
        , recovery_model_desc
        , collation_name
    from sys.databases s

为避免在执行查询时出现死锁,最好在这种情况下发出nolock,尽管有人认为它会导致脏读,对每个更新语句使用 begin 和 end 或使用 try block

CREATE PROCEDURE [dbo].[UpdateTestEvents] @Number VARCHAR(50)
AS
BEGIN
    DECLARE @tolerance DECIMAL(10, 10) = 0.15
    DECLARE @totalEvents DECIMAL(10, 0) = (
            SELECT COUNT(*)
            FROM dbo.History fh(NOLOCK)
            WHERE fh.Number = @Number
            )

    IF (@totalEvents = 0)
    BEGIN
        DECLARE @candidate VARCHAR(50) = (
                SELECT TOP 1 ACType
                FROM dbo.History(NOLOCK) AS fh
                WHERE fh.Number = @Number
                GROUP BY ACType
                HAVING (COUNT(*) / @totalEvents) > @tolerance
                ORDER BY MAX(ActualDepartureTime) DESC
                )

        --SELECT @candidate
        UPDATE dbo.History
        SET Ignore = 0
        WHERE Number = @Number
            AND dbo.StringsMatch(@candidate, ACType, DEFAULT) = 1;

        BEGIN
            WITH ValidOriginsAndDestinations
            AS (
                SELECT Origin
                FROM dbo.History(NOLOCK)
                WHERE Ignore = 0
                    AND Number = @Number
                
                UNION ALL
                
                SELECT Destination
                FROM dbo.History(NOLOCK)
                WHERE Ignore = 0
                    AND Number = @Number
                )
            UPDATE fh
            SET Ignore = 0
            FROM dbo.History AS fh
            WHERE Number = @Number
                AND Ignore = 1
                AND (
                    Origin IN (
                        SELECT *
                        FROM ValidOriginsAndDestinations
                        )
                    OR Destination IN (
                        SELECT *
                        FROM ValidOriginsAndDestinations
                        )
                    )
        END

        BEGIN
            WITH AirportsWithConfirmedFlights
            AS (
                SELECT a.lat
                    ,a.long
                FROM dbo.places AS a
                INNER JOIN dbo.History AS fh ON a.tidentifier = fh.Origin
                    OR a.tidentifier = fh.Destination
                WHERE fh.Number = @Number
                GROUP BY a.tidentifier
                    ,a.lat
                    ,a.long
                )
            UPDATE fh
            SET Ignore = 0
            FROM dbo.History AS fh
            INNER JOIN dbo.places AS a ON a.tidentifier = fh.Origin
                OR a.tidentifier = fh.Destination
        END
    END
END

`

Nolock` 和不良做法

各种文档,建议停止使用 nolock 提示的最佳实践,因为这会导致脏读和其他影响。但是,如果我们使用nolock 提示以及性能和影响以及有据可查的文档进行测试,它们将非常有用。最好的解决方法 这是为了改变数据库中的隔离级别。有几个考虑因素。

【讨论】:

更新查询权限发生死锁。如果我们将 NOLOCK 放在选择查询上会有帮助吗 如果您在 dbo.history 的选择查询中放置 nolock 应该会有所帮助 如果你在一个提交的事务之后读,它不会是脏读。对每个语句使用 begin 和 end 或使用 try 块来处理它。 NOLOCK 不会改变任何东西,因为死锁图中根本没有 没有 S-lock,冲突发生在访问同一个聚簇表的 2 个更新及其中的索引之间逆序 我同意你的观点,也有可能获得共享锁,无论如何我建议使用 try 和 catch 块来跟踪序列中的更新

以上是关于更新查询在同一张表上的 Sql 查询死锁的主要内容,如果未能解决你的问题,请参考以下文章

在MS SQL上的更新查询中减少PAGE级别的死锁

sql报告3.0死锁

使用 X 和 U 锁在同一张表上发生死锁

难以在同一张表上创建更新总计查询

postgresql死锁处理

使用多线程代码在一张表上发生 MySQL 死锁