patternsqlModerate
Handling concurrent access to a key table without deadlocks in SQL Server
Viewed 0 times
handlingwithoutsqlaccessdeadlocksserverconcurrenttablekey
Problem
I have a table that is used by a legacy application as a substitute for
Each row in the table stores the last used ID
Occasionally the stored proc gets a deadlock - I believe I've built an appropriate error handler; however I'm interested to see if this methodology works as I think it does, or if I'm barking up the wrong tree here.
I'm fairly certain there should be a way to access this table without any deadlocks at all.
The database itself is configured with
First, here is the table:
And the nonclustered index on the
Some sample data:
The stored procedure used to update the values stored in the table, and return the next ID:
```
CREATE PROCEDURE [dbo].GetNextID
)
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 @N
IDENTITY fields in various other tables.Each row in the table stores the last used ID
LastID for the field named in IDName.Occasionally the stored proc gets a deadlock - I believe I've built an appropriate error handler; however I'm interested to see if this methodology works as I think it does, or if I'm barking up the wrong tree here.
I'm fairly certain there should be a way to access this table without any deadlocks at all.
The database itself is configured with
READ_COMMITTED_SNAPSHOT = 1.First, here is the table:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);And the nonclustered index on the
IDName field: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
);
GOSome sample data:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GOThe stored procedure used to update the values stored in the table, and return the next ID:
```
CREATE PROCEDURE [dbo].GetNextID
)
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 @N
Solution
First, I would avoid making a round trip to the database for every value. For example, if your application knows it needs 20 new IDs, do not make 20 round trips. Make only one stored procedure call, and increment the counter by 20. Also it might be better to split your table into multiple ones.
It is possible to avoid deadlocks altogether. I have no deadlocks at all in my system. There are several ways to accomplish that. I will show how I would use sp_getapplock to eliminate deadlocks. I have no idea if this will work for you, because SQL Server is closed source, so I cannot see the source code, and as such I do not know if I have tested all possible cases.
The following describes what works for me. YMMV.
First, let us start with a scenario where we always get a considerable amount of deadlocks. Second, we shall use sp_getapplock eliminate them. The most important point here is to stress test your solution. Your solution may be different, but you need to expose it to high concurrency, as I will demonstrate later.
Prerequisites
Let us set up a table with some test data:
The following two procedures are quite likely to embrace in a deadlock:
Reproducing deadlocks
The following loops should reproduce more than 20 deadlocks every time you run them. If you get less than 20, increase the number of iterations.
In one tab, run this;
In another tab, run this script.
Make sure you start both within a couple of seconds.
Using sp_getapplock to eliminate deadlocks
Alter both procedures, rerun the loop, and see that you no longer have deadlocks:
Using a table with one row to eliminate deadlocks
Instead of invoking sp_getapplock, we can modify the following table:
Once we have this table created and populated, we can replace the following line
with this one, in both procedures:
You can rerun the stress test, and see for yourself that we have no dea
It is possible to avoid deadlocks altogether. I have no deadlocks at all in my system. There are several ways to accomplish that. I will show how I would use sp_getapplock to eliminate deadlocks. I have no idea if this will work for you, because SQL Server is closed source, so I cannot see the source code, and as such I do not know if I have tested all possible cases.
The following describes what works for me. YMMV.
First, let us start with a scenario where we always get a considerable amount of deadlocks. Second, we shall use sp_getapplock eliminate them. The most important point here is to stress test your solution. Your solution may be different, but you need to expose it to high concurrency, as I will demonstrate later.
Prerequisites
Let us set up a table with some test data:
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);
GOThe following two procedures are quite likely to embrace in a deadlock:
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;
GOReproducing deadlocks
The following loops should reproduce more than 20 deadlocks every time you run them. If you get less than 20, increase the number of iterations.
In one tab, run this;
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 ;In another tab, run this script.
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 ;Make sure you start both within a couple of seconds.
Using sp_getapplock to eliminate deadlocks
Alter both procedures, rerun the loop, and see that you no longer have deadlocks:
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;
GOUsing a table with one row to eliminate deadlocks
Instead of invoking sp_getapplock, we can modify the following table:
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);Once we have this table created and populated, we can replace the following line
EXEC sp_getapplock @Resource='DeadlockTest', @LockMode='Exclusive';with this one, in both procedures:
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;You can rerun the stress test, and see for yourself that we have no dea
Code Snippets
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);
GOCREATE 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;
GODECLARE @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 ;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;
GOContext
StackExchange Database Administrators Q#36603, answer score: 15
Revisions (0)
No revisions yet.