web-dev-qa-db-ja.com

処理のためにレコードを「チェックアウト」するための戦略

これに名前の付いたパターンがあるのか​​、それともひどい考えなのでないのかわかりません。しかし、アクティブ/アクティブロードバランス環境で動作するサービスが必要です。これはアプリケーションサーバーのみです。データベースは別のサーバーに配置されます。テーブルの各レコードのプロセスを実行する必要があるサービスがあります。このプロセスには1〜2分かかる場合があり、n分ごとに繰り返されます(構成可能、通常は15分)。

この処理を必要とする1000レコードのテーブルと、この同じデータセットに対して実行される2つのサービスで、各サービスに処理するレコードを「チェックアウト」させたいと思います。一度に1つのサービス/スレッドのみが各レコードを処理していることを確認する必要があります。

過去に「ロックテーブル」を使用していた同僚がいます。他のテーブルのレコードを論理的にロックするためにレコードがこのテーブルに書き込まれ(他のテーブルはかなり静的であり、非常に時々新しいレコードが追加されます)、削除されてロックを解放します。

新しいテーブルが常に削除を挿入するのではなく、いつロックされ、現在ロックされているかを示す列を作成する方が良いのではないかと思います。

誰かがこの種のことについてのヒントを持っていますか?長期的な論理ロックの確立されたパターンはありますか?一度に1つのサービスのみがロックを取得するようにするためのヒントはありますか? (私の同僚はTABLOCKXを使用してテーブル全体をロックしています。)

10
Dean

私は、余分な「ロック」テーブルや、次のレコードを取得するためにテーブル全体をロックするというアイデアの大ファンではありません。私はそれが行われている理由を理解していますが、ロックされたレコードを解放するために更新している操作の同時実行性も損なわれます(2つのプロセスが同じレコードを同時)。

私の好みは、ProcessStatusID(通常はTINYINT)列を、処理中のデータを含むテーブルに追加することです。また、LastModifiedDateのフィールドはありますか?そうでない場合は、追加する必要があります。はいの場合、これらのレコードはこの処理の外で更新されますか?この特定のプロセス外でレコードを更新できる場合は、StatusModifiedDate(またはそのようなもの)を追跡するために別のフィールドを追加する必要があります。この回答の残りの部分では、意味が明確な「StatusModifiedDate」を使用します(実際には、「LastModifiedDate」フィールドがなくてもフィールド名として使用できます)。

ProcessStatusIDの値( "ProcessStatus"と呼ばれる新しいルックアップテーブルに配置し、このテーブルに外部キーを設定する必要があります)は次のとおりです。

  1. 完了(この場合は「保留中」でもあります。どちらも「処理する準備ができている」ことを意味するため)
  2. 処理中(または「処理中」)
  3. エラー(または「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;
_

ここでの主な問題は、UPDATE操作でTOP (1)を実行できる一方で、_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)-テーブル )にあります:

  • READPAST(この正確なシナリオに適合するようです)

    データベースエンジンが他のトランザクションによってロックされている行を読み取らないことを指定します。 READPASTが指定されている場合、行レベルのロックはスキップされます。つまり、データベースエンジンは、ロックが解放されるまで現在のトランザクションをブロックするのではなく、行をスキップします。READPASTは、主に、SQL Serverテーブルを使用するワークキューを実装するときにロックの競合を減らすために使用されます。 READPASTを使用するキューリーダーは、他のトランザクションがロックを解放するまで待機する必要なく、他のトランザクションによってロックされた過去のキューエントリを次に使用可能なキューエントリにスキップします。

  • ROWLOCK(念のため)

    ページまたはテーブルのロックが通常行われるときに行ロックが行われることを指定します。

  • アップロック

    トランザクションが完了するまで、更新ロックを取得して保持することを指定します。 UPDLOCKは、行レベルまたはページレベルでのみ読み取り操作の更新ロックを取得します。

12
Solomon Rutzky

Service Brokerキューを使用して同様のことを(アプリケーションなしで、純粋にDB内で)行いました。軽量でACIDに完全に準拠しており、ほぼ無限にスケールアウトできます。透過的な行ロック(または「非表示」)が組み込まれています。バージョン2005以降で使用できます。

あなたの場合、全体的なアーキテクチャは次のようになる可能性があります。いくつかのプロセスは、スケジュールに従ってService Brokerダイアログにメッセージを送信し、リスナーはターゲット側のキューからメッセージを取得します。別個のメッセージタイプを作成することとは別に、メッセージボディにはほとんどすべてのものを含めることができます-たとえば、タイムアウト、およびタスクが持つ可能性のあるすべてのパラメーター。

把握するのが最も簡単なことではありませんが、それは確かですが、それを取得すると、その利点が明らかになります。

1
Roger Wolf