IDENTITY
字段。表中的每一行都存储了
LastID
中命名字段的最后使用的ID IDName
。有时,存储的proc会出现死锁-我相信我已经构建了适当的错误处理程序;但是我很想看看这种方法是否能像我认为的那样起作用,或者我是否在这里树错了树。我相当确定应该有一种方法来访问此方法该表完全没有任何死锁。
数据库本身配置有
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,
);
和
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
一些示例数据:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
用于更新表中存储的值并返回下一个ID的存储过程:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Max 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
存储的proc的示例执行:
<
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
编辑:
我添加了一个新索引,因为SP没有使用现有的索引IX_tblIDs_Name。我假设查询处理器正在使用聚簇索引,因为它需要存储在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);
编辑#2:
@AaronBertrand给出的建议并对其进行了稍微的修改。此处的总体思路是优化语句以消除不必要的锁定,并总体上提高SP的效率。
下面的代码替换了上面的从
BEGIN TRANSACTION
到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;
由于我们的代码从不向
LastID
中的该表添加记录,因此我们可以假设,如果@NewID为1,则打算将新ID附加到列表中,否则我们将更新列表中的现有行。#1 楼
首先,我将避免针对每个值往返数据库。例如,如果您的应用程序知道它需要20个新ID,则不要进行20次往返。只进行一个存储过程调用,并将计数器增加20。另外,将表拆分为多个表也可能更好。可以完全避免死锁。我的系统中完全没有死锁。有几种方法可以做到这一点。我将展示如何使用sp_getapplock消除死锁。我不知道这是否对您有用,因为SQL Server是封闭源代码,所以我看不到源代码,因此我不知道我是否已经测试了所有可能的情况。
以下介绍对我有效的方法。 YMMV。
首先,让我们从一个总是出现大量死锁的场景开始。其次,我们将使用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
死锁很可能包含以下两个过程:
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
再现死锁
每次运行时,以下循环应重现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 ;
在另一个选项卡中运行这个脚本。
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 ;
请确保您都在几秒钟之内启动它们。
使用sp_getapplock消除死锁
更改两个过程,重新运行循环,然后查看不再有死锁:
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
使用具有一行的表消除死锁
我们可以调用下表来代替调用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);
一旦创建并填充了该表,就可以在以下两个过程中用以下一行替换以下行:
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';
br />
您可以重新运行压力测试,并亲自查看我们没有死锁。
结论
如我们所见,可以使用sp_getapplock序列化对其他资源的访问。因此,它可以用来消除死锁。
当然,这会大大减慢修改速度。为了解决这个问题,我们需要为排他锁选择合适的粒度,并在可能的情况下使用集合而不是单个行。
在使用这种方法之前,您需要自己进行压力测试。首先,您需要确保您的原始方法至少遇到十几个僵局。其次,使用修改后的存储过程重新运行相同的repro脚本时,应该不会出现死锁。
一般来说,我认为没有什么好的方法可以确定T-SQL是否可以避免死锁只是看它或看执行计划。 IMO确定您的代码是否容易出现死锁的唯一方法是使代码具有高并发性。
消除死锁的好运!我们的系统完全没有任何僵局,这对我们的工作与生活平衡非常有用。
评论
+1作为sp_getapplock是一个鲜为人知的有用工具。鉴于“混乱的情况可能需要花费一些时间才能解决,将序列化的进程陷入僵局是一个方便的技巧。但是,对于这样一种容易理解并且可以(也许应该)通过标准锁定机制处理的情况,它是否应该是首选?
– Mark Storey-Smith
13年3月16日在1:07
@ MarkStorey-Smith这是我的首选,因为我仅对其进行了研究和压力测试,并且在任何情况下都可以重用它-序列化已经发生,因此sp_getapplock之后发生的所有事情都不会影响结果。使用标准的锁定机制,我永远不能确定-添加索引或仅获得另一个执行计划都可能导致死锁,而在此之前没有死锁。问我我怎么知道。
–A-K
13年3月16日在1:35
我想我缺少明显的东西,但是如何使用UPDATE dbo.DeadlockTestMutex SET Toggle = 1-Toggle WHERE ID = 1;防止死锁?
–戴尔K
19-09-13在4:14
#2 楼
在您的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;
还会返回一些其他变体(如果不被打败!)。
评论
虽然XLOCK可以防止现有的计数器从多个连接中更新,但是您是否不需要TABLOCKX来防止多个连接添加相同的新计数器?
–戴尔K
19年9月13日在4:13
@DaleBurrell不,您将对IDName进行PK或唯一约束。
– Mark Storey-Smith
19-09-13在13:29
#3 楼
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: Max 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
(为完整性起见,这是与存储的proc相关的表)
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
这是最新版本的执行计划:
这是原始版本的执行计划版本(易受死锁影响):
显然,新版本获胜!
作为比较,带有
(XLOCK)
的中间版本产生了以下计划:我想这是胜利!谢谢大家的帮助!
评论
确实应该可以,但是在不适用的地方使用了SERIALIZABLE。幻像行在这里不存在,那么为什么要使用一个存在的隔离级别来防止它们出现?同样,如果有人从另一个或从外部事务已启动的连接中调用您的过程,则他们发起的任何其他操作将在SERIALIZABLE处进行。可能会变得混乱。
– Mark Storey-Smith
13年3月14日在23:32
没有SERIALIZABLE可以防止幻影。它的存在是为了提供可序列化的隔离语义,即对数据库的持久影响,就好像所涉及的事务以某种未指定的顺序串行执行一样。
–保罗·怀特♦
2014年12月24日在6:46
#4 楼
不要偷走Mark Storey-Smith的风头,但他的职位上面有东西(顺带一提,是票数最多的东西)。我给Max的建议集中在“ 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
评论
同意这应该可以避免死锁,但是如果您忽略该事务,则很容易在插件上出现竞争状态。
– Mark Storey-Smith
13年3月16日在14:50
#5 楼
我去年通过更改以下方法修复了系统中的类似死锁: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;
对此:
UPDATE tblIDs SET LastID = @NewID WHERE IDName = @IDName;
IF @@ROWCOUNT = 0
BEGIN
INSERT ...
END
通常,仅选择
COUNT
来确定是否存在非常浪费。在这种情况下,因为它是0或1,所以它并不是很多工作,但是(a)这个习惯可能会渗入其他情况下,而这会导致成本更高(在这些情况下,请使用IF NOT EXISTS
而不是IF COUNT() = 0
),并且( b)完全不需要额外的扫描。 UPDATE
执行基本上相同的检查。此外,这对我来说似乎是一种严重的代码气味:
SET @NewID = COALESCE((SELECT LastID FROM tblIDs WHERE IDName = @IDName),0)+1;
这里的意思是什么?为什么不只使用身份列或在查询时使用
ROW_NUMBER()
导出该序列?评论
我们拥有的大多数表都使用IDENTITY。该表支持一些用MS Access编写的遗留代码,这些代码很可能需要进行改造。 SET @ NewID =行仅增加给定ID的表中存储的值(但您已经知道)。您可以扩展我如何使用ROW_NUMBER()吗?
– Max Vernon♦
13年3月13日在17:35
@MaxVernon并非不知道LastID在模型中的真正含义。目的是什么?这个名字并不完全是不言自明的。 Access如何使用它?
–亚伦·伯特兰(Aaron Bertrand)
2013年3月13日17:37
Access中的一个函数希望将行添加到任何没有IDENTITY的给定表中。首次访问调用GetNextID('WhatevertheIDFieldIsCalled')以获取下一个要使用的ID,然后将其与所需的任何数据一起插入到新行中。
– Max Vernon♦
13年3月13日在17:39
我将执行您的更改。一个纯粹的案例是“少即是多”!
– Max Vernon♦
13年13月13日在17:42
您固定的死锁可能会重新出现。您的第二种模式也很容易受到攻击:sqlblog.com/blogs/alexander_kuznetsov/archive/2010/01/12/…要消除死锁,我将使用sp_getapplock。可能有数百个用户的混合负载系统没有死锁。
–A-K
13年3月13日在18:09
评论
与如何配置数据库以支持RCSI无关。您有意在此处升级为SERIALIZABLE。是的,我只想添加所有相关信息。很高兴您确认这无关紧要!
使sp_getapplock成为死锁的受害者很容易,但是如果您开始事务,则不要调用sp_getapplock一次以获取排他锁,然后继续进行修改。
IDName是否唯一?然后建议“创建唯一的非聚集索引”。但是,如果您需要空值,那么索引也需要过滤。