他のさまざまなテーブルの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,
);
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
ストアドプロシージャの実行例:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
編集:
既存のインデックスIX_tblIDs_NameがSPで使用されていないため、新しいインデックスを追加しました。 LastIDに格納されている値が必要なため、クエリプロセッサはクラスター化インデックスを使用していると思います。とにかく、このインデックスIS実際の実行プランで使用されます:
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
の0でこのテーブルにレコードを追加することはないため、@ NewIDが1の場合、リストに新しいIDを追加することを想定できます。そうでない場合、既存の行を更新します。リスト。
まず、すべての値についてデータベースへの往復を行わないようにします。たとえば、アプリケーションで20の新しいIDが必要であることがわかっている場合は、20往復しないでください。ストアドプロシージャ呼び出しを1つだけ行い、カウンタを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
次の2つの手順は、デッドロックに陥る可能性が非常に高いです。
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未満の場合は、反復回数を増やします。
1つのタブで、これを実行します。
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 ;
両方を数秒以内に開始してください。
両方のプロシージャを変更し、ループを再実行して、デッドロックがなくなったことを確認します。
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';
これで、両方の手順で:
UPDATE dbo.DeadlockTestMutex SET Toggle = 1 - Toggle WHERE ID = 1;
ストレステストを再実行して、デッドロックがないことを確認してください。
これまで見てきたように、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;
他のいくつかのバリアントで復帰します(それに負けない場合)。
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
(完全を期すために、ストアドプロシージャに関連付けられたテーブルを次に示します)
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)
などを使用した中間バージョンでは、次の計画が作成されます。
私はそれが勝利だと思います!みんなの助けてくれてありがとう!
Mark Storey-Smithの雷を盗むわけではありませんが、彼は上の投稿(偶然に最も多くの賛成票を受け取っている)で何かに夢中になっています。マックスに与えたアドバイスは、私が本当にクールだと思う "UPDATE set @variable = column = column + value"コンストラクトを中心としたものですが、ドキュメント化されていない可能性があります(サポートされている必要があります。 TCPベンチマーク)。
ここにマークの答えのバリエーションがあります-新しい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
これを変更することで、昨年システムで同様のデッドロックを修正しました。
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)その習慣は、他の場合に出血する可能性がありますwillははるかにコストがかかります(これらの場合、IF NOT EXISTS
を使用しますIF COUNT() = 0
)の代わりに、(b)追加のスキャンは完全に不要です。 UPDATE
は基本的に同じチェックを実行します。
また、これは私には深刻なコードのにおいのように見えます:
SET @NewID = COALESCE((SELECT LastID FROM tblIDs WHERE IDName = @IDName),0)+1;
ここのポイントは何ですか?なぜ、ID列を使用するか、クエリ時にROW_NUMBER()
を使用してそのシーケンスを導出しないのですか?