Han*_*non 33 sql-server deadlock
我有一个旧应用程序用作替代IDENTITY各种其他表中的字段的表。
表中的每一行都存储了 中LastID命名的字段的最后使用的 ID IDName。
有时,存储过程会出现死锁——我相信我已经构建了一个合适的错误处理程序;但是我很想知道这种方法是否像我认为的那样有效,或者我是否在这里吠错了树。
我相当肯定应该有一种方法可以访问这个表,而不会出现任何死锁。
数据库本身配置了READ_COMMITTED_SNAPSHOT = 1.
首先,这是表:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
Run Code Online (Sandbox Code Playgroud)
以及该IDName字段上的非聚集索引:
CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName]
ON [dbo].[tblIDs]
(
[IDName] 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
, FILLFACTOR = 80
);
GO
Run Code Online (Sandbox Code Playgroud)
一些示例数据:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
Run Code Online (Sandbox Code Playgroud)
用于更新存储在表中的值的存储过程,并返回下一个 ID:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Hannah Vernon
Date: 2012-07-19
*/
DECLARE @Retry int;
DECLARE @EN int, @ES int, @ET int;
SET @Retry = 5;
DECLARE @NewID int;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET NOCOUNT ON;
WHILE @Retry > 0
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM tblIDs
WHERE IDName = @IDName),0)+1;
IF (SELECT COUNT(IDName)
FROM tblIDs
WHERE IDName = @IDName) = 0
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID)
ELSE
UPDATE tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
SET @Retry = -2; /* no need to retry since the operation completed */
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
SET @Retry = @Retry - 1;
ELSE
BEGIN
SET @Retry = -1;
SET @EN = ERROR_NUMBER();
SET @ES = ERROR_SEVERITY();
SET @ET = ERROR_STATE()
RAISERROR (@EN,@ES,@ET);
END
ROLLBACK TRANSACTION;
END CATCH
END
IF @Retry = 0 /* must have deadlock'd 5 times. */
BEGIN
SET @EN = 1205;
SET @ES = 13;
SET @ET = 1
RAISERROR (@EN,@ES,@ET);
END
ELSE
SELECT @NewID AS NewID;
END
GO
Run Code Online (Sandbox Code Playgroud)
存储过程的示例执行:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
Run Code Online (Sandbox Code Playgroud)
编辑:
我添加了一个新索引,因为现有索引 IX_tblIDs_Name 没有被 SP 使用;我假设查询处理器正在使用聚集索引,因为它需要存储在 LastID 中的值。无论如何,这个索引被实际的执行计划使用:
CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID
ON dbo.tblIDs
(
IDName ASC
)
INCLUDE
(
LastID
)
WITH (FILLFACTOR = 100
, ONLINE=ON
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON);
Run Code Online (Sandbox Code Playgroud)
编辑#2:
我采纳了@AaronBertrand 给出的建议并对其进行了略微修改。这里的总体思路是细化语句以消除不必要的锁定,并总体上使 SP 更高效。
下面的代码将上面的代码从BEGIN TRANSACTIONto替换为END TRANSACTION:
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM dbo.tblIDs
WHERE IDName = @IDName), 0) + 1;
IF @NewID = 1
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID);
ELSE
UPDATE dbo.tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)
由于我们的代码从不向此表中添加 0 的记录,LastID我们可以假设如果 @NewID 为 1,则意图是将新 ID 附加到列表中,否则我们将更新列表中的现有行。
A-K*_*A-K 15
首先,我会避免为每个值都访问数据库。例如,如果您的应用程序知道它需要 20 个新 ID,则不要进行 20 次往返。只调用一次存储过程,并将计数器增加 20。此外,最好将表拆分为多个表。
完全可以避免死锁。我的系统中根本没有死锁。有几种方法可以实现这一点。我将展示如何使用 sp_getapplock 消除死锁。我不知道这是否适合你,因为 SQL Server 是封闭源代码,所以我看不到源代码,因此我不知道我是否已经测试了所有可能的情况。
以下描述了对我有用的内容。天啊。
首先,让我们从一个总是出现大量死锁的场景开始。其次,我们将使用 sp_getapplock 消除它们。这里最重要的一点是对您的解决方案进行压力测试。您的解决方案可能有所不同,但您需要将其暴露在高并发下,我将在后面演示。
让我们建立一个包含一些测试数据的表格:
CREATE TABLE dbo.Numbers(n INT NOT NULL PRIMARY KEY);
GO
INSERT INTO dbo.Numbers
( n )
VALUES ( 1 );
GO
DECLARE @i INT;
SET @i=0;
WHILE @i<21
BEGIN
INSERT INTO dbo.Numbers
( n )
SELECT n + POWER(2, @i)
FROM dbo.Numbers;
SET @i = @i + 1;
END;
GO
SELECT n AS ID, n AS Key1, n AS Key2, 0 AS Counter1, 0 AS Counter2
INTO dbo.DeadlockTest FROM dbo.Numbers
GO
ALTER TABLE dbo.DeadlockTest ADD CONSTRAINT PK_DeadlockTest PRIMARY KEY(ID);
GO
CREATE INDEX DeadlockTestKey1 ON dbo.DeadlockTest(Key1);
GO
CREATE INDEX DeadlockTestKey2 ON dbo.DeadlockTest(Key2);
GO
Run Code Online (Sandbox Code Playgroud)
以下两个过程很可能陷入僵局:
CREATE PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO
CREATE PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO
Run Code Online (Sandbox Code Playgroud)
每次运行以下循环时,它们应该会重现 20 多个死锁。如果小于 20,请增加迭代次数。
在一个选项卡中,运行这个;
DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;
WHILE @i<5000 BEGIN ;
BEGIN TRY
EXEC dbo.UpdateCounter1 @Key1=123456;
END TRY
BEGIN CATCH
SET @DeadlockCount = @DeadlockCount + 1;
ROLLBACK;
END CATCH ;
SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;
Run Code Online (Sandbox Code Playgroud)
在另一个选项卡中,运行此脚本。
DECLARE @i INT, @DeadlockCount INT;
SELECT @i=0, @DeadlockCount=0;
WHILE @i<5000 BEGIN ;
BEGIN TRY
EXEC dbo.UpdateCounter2 @Key2=123456;
END TRY
BEGIN CATCH
SET @DeadlockCount = @DeadlockCount + 1;
ROLLBACK;
END CATCH ;
SET @i = @i + 1;
END;
SELECT 'Deadlocks caught: ', @DeadlockCount ;
Run Code Online (Sandbox Code Playgroud)
确保在几秒钟内启动两者。
更改这两个过程,重新运行循环,并看到您不再有死锁:
ALTER PROCEDURE dbo.UpdateCounter1 @Key1 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
SET @Key1=@Key1-10000;
UPDATE dbo.DeadlockTest SET Counter1=Counter1+1 WHERE Key1=@Key1;
COMMIT;
GO
ALTER PROCEDURE dbo.UpdateCounter2 @Key2 INT
AS
SET NOCOUNT ON ;
SET XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN TRANSACTION ;
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
SET @Key2=@Key2-10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
SET @Key2=@Key2+10000;
UPDATE dbo.DeadlockTest SET Counter2=Counter2+1 WHERE Key2=@Key2;
COMMIT;
GO
Run Code Online (Sandbox Code Playgroud)
我们可以修改下表,而不是调用 sp_getapplock:
CREATE TABLE dbo.DeadlockTestMutex(
ID INT NOT NULL,
CONSTRAINT PK_DeadlockTestMutex PRIMARY KEY(ID),
Toggle INT NOT NULL);
GO
INSERT INTO dbo.DeadlockTestMutex(ID, Toggle)
VALUES(1,0);
Run Code Online (Sandbox Code Playgroud)
创建并填充此表后,我们可以替换以下行
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
Run Code Online (Sandbox Code Playgroud)
有了这个,在两个程序中:
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
Run Code Online (Sandbox Code Playgroud)
您可以重新运行压力测试,并亲眼看看我们没有死锁。
正如我们所见,sp_getapplock 可用于序列化对其他资源的访问。因此,它可以用来消除死锁。
当然,这会显着减慢修改速度。为了解决这个问题,我们需要为排他锁选择正确的粒度,并且尽可能使用集合而不是单个行。
在使用这种方法之前,您需要自己进行压力测试。首先,您需要确保您的原始方法至少遇到了几十个死锁。其次,当您使用修改后的存储过程重新运行相同的重现脚本时,应该不会出现死锁。
一般来说,我不认为有什么好方法可以仅通过查看或查看执行计划来确定您的 T-SQL 是否可以避免死锁。IMO 确定您的代码是否容易死锁的唯一方法是将其暴露于高并发。
祝你好运消除僵局!我们的系统根本没有任何僵局,这对我们的工作与生活平衡非常有用。
XLOCK在您的SELECT方法或以下方法中使用提示UPDATE应该可以避免这种类型的死锁:
DECLARE @Output TABLE ([NewId] INT);
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
UPDATE
dbo.tblIDs WITH (XLOCK)
SET
LastID = LastID + 1
OUTPUT
INSERTED.[LastId] INTO @Output
WHERE
IDName = @IDName;
IF(@@ROWCOUNT = 1)
BEGIN
SELECT @NewId = [NewId] FROM @Output;
END
ELSE
BEGIN
SET @NewId = 1;
INSERT dbo.tblIDs
(IDName, LastID)
VALUES
(@IDName, @NewId);
END
SELECT [NewId] = @NewId ;
COMMIT TRANSACTION;
Run Code Online (Sandbox Code Playgroud)
将返回几个其他变体(如果没有被打败!)。
Mike Defehr 向我展示了一种以非常轻量级的方式完成此任务的优雅方式:
ALTER PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs for a given IDName
Author: Hannah Vernon / Mike Defehr
Date: 2012-07-19
*/
DECLARE @Retry int;
DECLARE @EN int, @ES int, @ET int;
SET @Retry = 5;
DECLARE @NewID int;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET NOCOUNT ON;
WHILE @Retry > 0
BEGIN
BEGIN TRY
UPDATE dbo.tblIDs
SET @NewID = LastID = LastID + 1
WHERE IDName = @IDName;
IF @NewID IS NULL
BEGIN
SET @NewID = 1;
INSERT INTO tblIDs (IDName, LastID) VALUES (@IDName, @NewID);
END
SET @Retry = -2; /* no need to retry since the operation completed */
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
SET @Retry = @Retry - 1;
ELSE
BEGIN
SET @Retry = -1;
SET @EN = ERROR_NUMBER();
SET @ES = ERROR_SEVERITY();
SET @ET = ERROR_STATE()
RAISERROR (@EN,@ES,@ET);
END
END CATCH
END
IF @Retry = 0 /* must have deadlock'd 5 times. */
BEGIN
SET @EN = 1205;
SET @ES = 13;
SET @ET = 1
RAISERROR (@EN,@ES,@ET);
END
ELSE
SELECT @NewID AS NewID;
END
GO
Run Code Online (Sandbox Code Playgroud)
(为了完整起见,这里是与存储过程关联的表)
CREATE TABLE [dbo].[tblIDs]
(
IDName nvarchar(255) NOT NULL,
LastID int NULL,
CONSTRAINT [PK_tblIDs] PRIMARY KEY CLUSTERED
(
[IDName] ASC
) WITH
(
PAD_INDEX = OFF
, STATISTICS_NORECOMPUTE = OFF
, IGNORE_DUP_KEY = OFF
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON
, FILLFACTOR = 100
)
);
GO
Run Code Online (Sandbox Code Playgroud)
这是最新版本的执行计划:

这是原始版本的执行计划(容易出现死锁):

显然,新版本获胜!
为了比较,与(XLOCK)etc的中间版本,产生以下计划:

我会说这是一场胜利!感谢大家的帮助!
小智 6
不是为了抢走 Mark Storey-Smith 的风头,但他在上面的帖子中做了一些事情(顺便说一句,这获得了最多的赞成票)。我给 Hannah 的建议集中在“UPDATE set @variable = column = column + value”构造上,我觉得它真的很酷,但我认为可能没有记录(必须支持它,尽管因为它专门用于 TCP基准)。
这是 Mark 答案的一个变体 - 因为您将新的 ID 值作为记录集返回,所以您可以完全取消标量变量,也不需要显式事务,我同意没有必要搞乱隔离级别以及。结果非常干净,非常光滑......
ALTER PROC [dbo].[GetNextID]
@IDName nvarchar(255)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Output TABLE ([NewID] INT);
UPDATE dbo.tblIDs SET LastID = LastID + 1
OUTPUT inserted.[LastId] INTO @Output
WHERE IDName = @IDName;
IF(@@ROWCOUNT = 1)
SELECT [NewID] FROM @Output;
ELSE
INSERT dbo.tblIDs (IDName, LastID)
OUTPUT INSERTED.LastID AS [NewID]
VALUES (@IDName,1);
END
Run Code Online (Sandbox Code Playgroud)