キューとして機能しているテーブルがある場合、複数のクライアントがキューから同時に処理できるように、テーブルをどのように構成するのが最適ですか?
たとえば、次の表は、ワーカーが処理する必要があるコマンドを示しています。ワーカーが完了すると、処理された値がtrueに設定されます。
| ID | COMMAND | PROCESSED |
| 1 | ... | true |
| 2 | ... | false |
| 3 | ... | false |
クライアントは次のように動作する1つのコマンドを取得する場合があります。
select top 1 COMMAND
from EXAMPLE_TABLE
with (UPDLOCK, ROWLOCK)
where PROCESSED=false;
ただし、複数のワーカーがある場合、それぞれがID = 2の行を取得しようとします。最初のみが悲観的ロックを取得し、残りは待機します。次に、そのうちの1つが3行目などになります。
どのクエリ/構成では、各ワーカークライアントがそれぞれ異なる行を取得してそれらを同時に処理できるようになりますか?
編集:
いくつかの回答は、テーブル自体を使用して処理中の状態を記録する方法のバリエーションを示唆しています。これは単一のトランザクション内では不可能だと思いました。 (つまり、txnがコミットされるまで他のワーカーがその状態を確認できない場合、状態を更新する意味は何ですか?)おそらく提案は次のとおりです。
# start transaction
update to 'processing'
# end transaction
# start transaction
process the command
update to 'processed'
# end transaction
これは、人々が通常この問題に取り組む方法ですか?可能であれば、問題はDBでより適切に処理されるように思えます。
私はあなたに行くことをお勧めします キューとしてテーブルを使用する 。適切に実装されたキューは、数千の同時ユーザーを処理し、1分あたり1/2Millionのエンキュー/デキュー操作を処理できます。 SQL Server 2005までは、ソリューションは煩雑で、単一トランザクションでSELECT
とUPDATE
を混在させ、gbnによってリンクされた記事のようにロックヒントの適切な組み合わせを提供する必要がありました。幸運にも、OUTPUT句が登場したSQL Server 2005以降、はるかに洗練されたソリューションが利用可能になり、現在MSDNでは OUTPUT句 の使用を推奨しています。
キューとしてテーブルを使用するアプリケーションで、または中間結果セットを保持するために、OUTPUTを使用できます。つまり、アプリケーションは常にテーブルに行を追加または削除しています
基本的に、これを高度に並行して機能させるには、パズルの3つの部分を正しく理解する必要があります。
1)アトミックにデキューする必要があります。行を見つけ、ロックされた行をスキップし、単一のアトミック操作で「デキュー」としてマークする必要があります。これがOUTPUT
句の出番です。
with CTE as (
SELECT TOP(1) COMMAND, PROCESSED
FROM TABLE WITH (READPAST)
WHERE PROCESSED = 0)
UPDATE CTE
SET PROCESSED = 1
OUTPUT INSERTED.*;
2)PROCESSED
列の左端のクラスター化インデックスキーでテーブルを構造化する必要があります。 ID
が主キーとして使用された場合は、それをクラスター化キーの2番目の列として移動します。 ID
列で非クラスター化キーを保持するかどうかの議論は開かれていますが、私はキューよりもセカンダリ非クラスター化インデックスがないことを強く推奨します。
CREATE CLUSTERED INDEX cdxTable on TABLE(PROCESSED, ID);
3)デキュー以外の方法でこのテーブルをクエリすることはできません。ピーク操作を実行しようとするか、テーブルをキューおよびストアの両方として使用しようとすると可能性が非常に高くなりデッドロックが発生し、スループットが劇的に低下します。
アトミックデキュー、デキューする要素を検索するREADPASTヒント、および処理ビットに基づくクラスター化インデックスの左端のキーの組み合わせにより、高度な同時負荷で非常に高いスループットが保証されます。
ここでの私の答えは、テーブルをキューとして使用する方法を示しています... SQL Serverプロセスキューの競合状態
基本的には「ROWLOCK、READPAST、UPDLOCK」のヒントが必要です
複数のクライアントの操作をシリアル化する場合は、アプリケーションロックを使用するだけで済みます。
BEGIN TRANSACTION
EXEC sp_getapplock @resource = 'app_token', @lockMode = 'Exclusive'
-- perform operation
EXEC sp_releaseapplock @resource = 'app_token'
COMMIT TRANSACTION
1つの方法は、単一の更新ステートメントで行をマークすることです。 where
句でステータスを読み取り、set
句でステータスを変更すると、行がロックされるため、他のプロセスが間に入ることができなくなります。例えば:
declare @pickup_id int
set @pickup_id = 1
set rowcount 1
update YourTable
set status = 'picked up'
, @pickup_id = id
where status = 'new'
set rowcount 0
return @pickup_id
これはrowcount
を使用して、最大で1行を更新します。行が見つからなかった場合、@pickup_id
は-1
になります。
Processedにブール値を使用する代わりに、intを使用してコマンドの状態を定義できます。
1 = not processed
2 = in progress
3 = complete
次に、各ワーカーはProcessed = 1の次の行を取得し、Processedを2に更新して、作業を開始します。完了した作業がProcessedに更新されると、3に更新されます。このアプローチでは、他のProcessedの結果を拡張することもできます。たとえば、ワーカーが完了したことを定義するだけでなく、「Completed Succesfully」と「Completed with Errors」の新しいステータスを追加できます。
おそらく、より良いオプションは、バージョン/タイムスタンプ列と一緒にtrisSate処理された列を使用することです。処理済み列の3つの値は、行が処理中か、処理済みか、未処理かを示します。
例えば
CREATE TABLE Queue ID INT NOT NULL PRIMARY KEY,
Command NVARCHAR(100),
Processed INT NOT NULL CHECK (Processed in (0,1,2) ),
Version timestamp)
トップ1の未処理の行を取得し、ステータスを「処理中」に設定し、処理が完了するとステータスを「処理済み」に戻します。バージョンと主キー列に基づいて更新ステータスを決定します。更新が失敗した場合、誰かがすでにそこにいます。
クライアント識別子も追加すると、クライアントの処理中にクライアントが停止した場合に再起動し、最後の行を確認してから、元の場所から開始できます。
テーブルのロックをいじるのを避けます。 IsProcessing(ビット/ブール値)とProcessingStarted(日時)のような2つの追加の列を作成するだけです。ワーカーがクラッシュするか、タイムアウト後に行を更新しない場合、別のワーカーにデータの処理を試行させることができます。