私は、余分な「ロック」テーブルや、次のレコードを取得するためにテーブル全体をロックするというアイデアの大ファンではありません。私はそれが行われている理由を理解していますが、ロックされたレコードを解放するために更新している操作の同時実行性にも害を与えています(2つのプロセスが同じレコードを同時)。
私の好みは、ProcessStatusID(通常はTINYINT)列を、処理中のデータを含むテーブルに追加することです。また、LastModifiedDateのフィールドはありますか?そうでない場合は、追加する必要があります。はいの場合、これらのレコードはこの処理の外で更新されますか?この特定のプロセス外でレコードを更新できる場合は、別のフィールドを追加して、StatusModifiedDate(またはそのようなもの)を追跡する必要があります。この回答の残りの部分では、「StatusModifiedDate」を使用します。これは、その意味が明らかであるためです(実際、「LastModifiedDate」フィールドがない場合でも、実際にはフィールド名として使用できます)。
ProcessStatusIDの値(「ProcessStatus」という新しいルックアップテーブルに配置し、このテーブルに外部キーを設定する必要があります)は次のようになります。
- 完了(この場合は「保留中」でもあります。どちらも「処理する準備ができている」ことを意味するため)
- 処理中(または「処理中」)
- エラー(または「WTF?」)
この時点で、アプリケーションから次のレコードを取得して処理したいだけで、その決定を下すために何も渡さないと想定するのは安全だと思われます。そのため、「完了」/「保留」に設定されている(少なくともStatusModifiedDateに関して)最も古いレコードを取得したいとします。以下に沿ったもの:
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
また、他のプロセスがレコードを取得しないように、同時にそのレコードを「処理中」に更新したいと考えています。このOUTPUT
句を使用して、同じトランザクションでUPDATEとSELECTを実行できます。
UPDATE TOP (1) pt
SET pt.StatusID = 2,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM ProcessTable pt
WHERE pt.StatusID = 1;
ここでの主な問題はTOP (1)
、UPDATE
操作でaを実行できる一方で、を実行する方法がないことORDER BY
です。しかし、それをCTEでラップして、これら2つの概念を組み合わせることができます。
;WITH cte AS
(
SELECT TOP 1 pt.RecordID
FROM ProcessTable pt (READPAST, ROWLOCK, UPDLOCK)
WHERE pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;
)
UPDATE cte
SET cte.StatusID = 2,
cte.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID;
明らかな問題は、SELECTを同時に実行する2つのプロセスが同じレコードを取得できるかどうかです。特にREADPASTおよびUPDLOCKヒントと組み合わせて(詳細については以下を参照)、OUTPUT句を使用したUPDATEで問題がないことを確信しています。ただし、私はこの正確なシナリオをテストしていません。なんらかの理由で上記のクエリが競合状態を処理しない場合は、アプリケーションロックを追加します。
上記のCTEクエリは、sp_getapplockおよびsp_releaseapplockでラップして、プロセスの「ゲートキーパー」を作成できます。そうすることで、上記のクエリを実行するために一度に1つのプロセスのみが入力できます。他のプロセスは、applockを持つプロセスが解放するまでブロックされます。また、プロセス全体のこのステップはRecordIDを取得するだけなので、非常に高速で、他のプロセスを非常に長くブロックすることはありません。また、CTEクエリと同様に、テーブル全体をブロックしないため、他の行への他の更新を許可します(そのステータスを「完了」または「エラー」に設定します)。基本的に:
BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';
{CTE UPDATE query shown above}
EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;
アプリケーションロックは非常に便利ですが、慎重に使用する必要があります。
最後に、ステータスを「完了」または「エラー」に設定するためのストアドプロシージャが必要です。そしてそれは簡単なことです:
CREATE PROCEDURE ProcessTable_SetProcessStatusID
(
@RecordID INT,
@ProcessStatusID TINYINT
)
AS
SET NOCOUNT ON;
UPDATE pt
SET pt.ProcessStatusID = @ProcessStatusID,
pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
FROM ProcessTable pt
WHERE pt.RecordID = @RecordID;
テーブルヒント(ヒント(Transact-SQL)-テーブルにあります):