SQL Serverの複数のワーカーのFIFOキューテーブル


15

次のstackoverflowの質問に答えようとしました:

やや素朴な答えを投稿した後、私は自分の口がどこにあるかを考え、実際に私が提案しているシナリオをテストしました。まあ、それは思ったよりもはるかに難しいことが判明しました(誰にも驚きはありません、確かです)。

ここで私が試し、考えたことがあります:

  • まず、派生テーブル内でORDER BYを使用して、TOP 1 UPDATEを試しましたROWLOCK, READPAST。これによりデッドロックが発生し、アイテムの順序が狂って処理されました。同じ行を複数回処理しようとすることを必要とするエラーを除いて、可能な限りFIFOに近い必要があります。

  • 私は、その後の様々な組み合わせを使用して、変数に所望の次のキューIDを選択しようとしたREADPASTUPDLOCKHOLDLOCK、及びROWLOCK排他的にそのセッションによって更新するための行を保存します。私が試したすべてのバリエーションは、以前と同じ問題に悩まされていましたREADPAST

    READ COMMITTEDまたはREPEATABLE READ分離レベルでのみREADPASTロックを指定できます。

    READ COMMITTED であったため、これは混乱を招きました。以前にこれに遭遇したことがあり、イライラします。

  • この質問を書き始めてから、Remus Rusaniが質問に対する新しい回答を投稿しました。私は彼のリンクされた記事を読んで、彼が破壊的な読み取りを使用していることを確認しました。彼は答えで、「Webコールの間、ロックを保持することは現実的に不可能です」と述べた。更新または削除を行うためにロックが必要なホットスポットとページに関する彼の記事の内容を読んだ後、探していることを行うために正しいロックを実行できたとしても、それはスケーラブルではなく、大規模な並行性を処理しません。

今、どこに行けばいいのかわかりません。行の処理中にロックを維持することはできません(高tpsまたは大規模な同時実行性をサポートしていなくても)。私は何が欠けていますか?

私より賢い人と私より経験のある人が助けてくれることを期待して、私が使用していたテストスクリプトを以下に示します。TOP 1 UPDATEメソッドに切り替えられますが、他のメソッドを残し、コメントアウトしてありますので、あなたもそれを調べたいと思います。

これらをそれぞれ別のセッションに貼り付け、セッション1を実行してから、他のすべてをすばやく実行します。約50秒でテストは終了します。各セッションからのメッセージを見て、どのような作業を行ったか(またはどのように失敗したか)を確認してください。最初のセッションでは、存在するロックと処理中のキューアイテムの詳細を2回目に撮影したスナップショットを含む行セットが表示されます。それは時々機能し、他の時間はまったく機能しません。

セッション1

/* Session 1: Setup and control - Run this session first, then immediately run all other sessions */
IF Object_ID('dbo.Queue', 'U') IS NULL
   CREATE TABLE dbo.Queue (
      QueueID int identity(1,1) NOT NULL,
      StatusID int NOT NULL,
      QueuedDate datetime CONSTRAINT DF_Queue_QueuedDate DEFAULT (GetDate()),
      CONSTRAINT PK_Queue PRIMARY KEY CLUSTERED (QueuedDate, QueueID)
   );

IF Object_ID('dbo.QueueHistory', 'U') IS NULL
   CREATE TABLE dbo.QueueHistory (
      HistoryDate datetime NOT NULL,
      QueueID int NOT NULL
   );

IF Object_ID('dbo.LockHistory', 'U') IS NULL
   CREATE TABLE dbo.LockHistory (
      HistoryDate datetime NOT NULL,
      ResourceType varchar(100),
      RequestMode varchar(100),
      RequestStatus varchar(100),
      ResourceDescription varchar(200),
      ResourceAssociatedEntityID varchar(200)
   );

IF Object_ID('dbo.StartTime', 'U') IS NULL
   CREATE TABLE dbo.StartTime (
      StartTime datetime NOT NULL
   );

SET NOCOUNT ON;

IF (SELECT Count(*) FROM dbo.Queue) < 10000 BEGIN
   TRUNCATE TABLE dbo.Queue;

   WITH A (N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
   B (N) AS (SELECT 1 FROM A Z, A I, A P),
   C (N) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM B O, B W)
   INSERT dbo.Queue (StatusID, QueuedDate)
   SELECT 1, DateAdd(millisecond, C.N * 3, GetDate() - '00:05:00')
   FROM C
   WHERE C.N <= 10000;
END;

TRUNCATE TABLE dbo.StartTime;
INSERT dbo.StartTime SELECT GetDate() + '00:00:15'; -- or however long it takes you to go run the other sessions
GO
TRUNCATE TABLE dbo.QueueHistory;
SET NOCOUNT ON;

DECLARE
   @Time varchar(8),
   @Now datetime;
SELECT @Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;

DECLARE @i int,
@QueueID int;
SET @i = 1;
WHILE @i <= 33 BEGIN
   SET @Now  = GetDate();
   INSERT dbo.QueueHistory
   SELECT
      @Now,
      QueueID
   FROM
      dbo.Queue Q WITH (NOLOCK)
   WHERE
      Q.StatusID <> 1;

   INSERT dbo.LockHistory
   SELECT
      @Now,
      L.resource_type,
      L.request_mode,
      L.request_status,
      L.resource_description,
      L.resource_associated_entity_id
   FROM
      sys.dm_tran_current_transaction T
      INNER JOIN sys.dm_tran_locks L
         ON L.request_owner_id = T.transaction_id;
   WAITFOR DELAY '00:00:01';
   SET @i = @i + 1;
END;

WITH Cols AS (
   SELECT *, Row_Number() OVER (PARTITION BY HistoryDate ORDER BY QueueID) Col
   FROM dbo.QueueHistory
), P AS (
   SELECT *
   FROM
      Cols
      PIVOT (Max(QueueID) FOR Col IN ([1], [2], [3], [4], [5], [6], [7], [8])) P
)
SELECT L.*, P.[1], P.[2], P.[3], P.[4], P.[5], P.[6], P.[7], P.[8]
FROM
   dbo.LockHistory L
   FULL JOIN P
      ON L.HistoryDate = P.HistoryDate

/* Clean up afterward
DROP TABLE dbo.StartTime;
DROP TABLE dbo.LockHistory;
DROP TABLE dbo.QueueHistory;
DROP TABLE dbo.Queue;
*/

セッション2

/* Session 2: Simulate an application instance holding a row locked for a long period, and eventually abandoning it. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET NOCOUNT ON;
SET XACT_ABORT ON;

DECLARE
   @QueueID int,
   @Time varchar(8);
SELECT @Time = Convert(varchar(8), StartTime + '0:00:01', 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;
BEGIN TRAN;

--SET @QueueID = (
--   SELECT TOP 1 QueueID
--   FROM dbo.Queue WITH (READPAST, UPDLOCK)
--   WHERE StatusID = 1 -- ready
--   ORDER BY QueuedDate, QueueID
--);

--UPDATE dbo.Queue
--SET StatusID = 2 -- in process
----OUTPUT Inserted.*
--WHERE QueueID = @QueueID;

SET @QueueID = NULL;
UPDATE Q
SET Q.StatusID = 1, @QueueID = Q.QueueID
FROM (
   SELECT TOP 1 *
   FROM dbo.Queue WITH (ROWLOCK, READPAST)
   WHERE StatusID = 1
   ORDER BY QueuedDate, QueueID
) Q

PRINT @QueueID;

WAITFOR DELAY '00:00:20'; -- Release it partway through the test

ROLLBACK TRAN; -- Simulate client disconnecting

セッション3

/* Session 3: Run a near-continuous series of "failed" queue processing. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET XACT_ABORT ON;
SET NOCOUNT ON;
DECLARE
   @QueueID int,
   @EndDate datetime,
   @NextDate datetime,
   @Time varchar(8);

SELECT
   @EndDate = StartTime + '0:00:33',
   @Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;

WAITFOR TIME @Time;

WHILE GetDate() < @EndDate BEGIN
   BEGIN TRAN;

   --SET @QueueID = (
   --   SELECT TOP 1 QueueID
   --   FROM dbo.Queue WITH (READPAST, UPDLOCK)
   --   WHERE StatusID = 1 -- ready
   --   ORDER BY QueuedDate, QueueID
   --);

   --UPDATE dbo.Queue
   --SET StatusID = 2 -- in process
   ----OUTPUT Inserted.*
   --WHERE QueueID = @QueueID;

   SET @QueueID = NULL;
   UPDATE Q
   SET Q.StatusID = 1, @QueueID = Q.QueueID
   FROM (
      SELECT TOP 1 *
      FROM dbo.Queue WITH (ROWLOCK, READPAST)
      WHERE StatusID = 1
      ORDER BY QueuedDate, QueueID
   ) Q

   PRINT @QueueID;

   SET @NextDate = GetDate() + '00:00:00.015';
   WHILE GetDate() < @NextDate SET NOCOUNT ON;
   ROLLBACK TRAN;
END

セッション4以降-好きなだけ

/* Session 4: "Process" the queue normally, one every second for 30 seconds. */
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET XACT_ABORT ON;
SET NOCOUNT ON;

DECLARE @Time varchar(8);
SELECT @Time = Convert(varchar(8), StartTime, 114)
FROM dbo.StartTime;
WAITFOR TIME @Time;

DECLARE @i int,
@QueueID int;
SET @i = 1;
WHILE @i <= 30 BEGIN
   BEGIN TRAN;

   --SET @QueueID = (
   --   SELECT TOP 1 QueueID
   --   FROM dbo.Queue WITH (READPAST, UPDLOCK)
   --   WHERE StatusID = 1 -- ready
   --   ORDER BY QueuedDate, QueueID
   --);

   --UPDATE dbo.Queue
   --SET StatusID = 2 -- in process
   --WHERE QueueID = @QueueID;

   SET @QueueID = NULL;
   UPDATE Q
   SET Q.StatusID = 1, @QueueID = Q.QueueID
   FROM (
      SELECT TOP 1 *
      FROM dbo.Queue WITH (ROWLOCK, READPAST)
      WHERE StatusID = 1
      ORDER BY QueuedDate, QueueID
   ) Q

   PRINT @QueueID;
   WAITFOR DELAY '00:00:01'
   SET @i = @i + 1;
   DELETE dbo.Queue
   WHERE QueueID = @QueueID;   
   COMMIT TRAN;
END

2
リンクされている記事で説明されているキューは、1秒あたり数百またはそれ以下の操作に拡張できます。ホットスポットの競合の問題は、より高いスケールでのみ関連します。ハイエンドシステムで高いスループットを達成できる既知の緩和戦略があり、1秒あたり数万になりますが、これらの緩和には慎重な評価が必要であり、SQLCATの監視下で展開されます。
レムスルサヌ

1つの興味深いしわは、READPAST, UPDLOCK, ROWLOCKデータをQueueHistoryテーブルに取り込むためのスクリプトでは何もしないということです。StatusIDがコミットされていないためだろうか?それは使用しているWITH (NOLOCK)ので、理論的には動作するはずです...そしてそれは前に動作しました!なぜ今機能していないのかはわかりませんが、おそらく別の学習経験でしょう。
エリック

コードを、解決しようとしているデッドロックやその他の問題を示す最小のサンプルに減らすことができますか?
ニックチャマス

@Nickコードを削減しようとします。他のコメントについては、クラスター化インデックスの一部であり、日付の後に並べられたID列があります。「破壊的な読み取り」(出力付きのDELETE)を楽しもうと思っていますが、要求された要件の1つは、アプリケーションインスタンスが失敗した場合に、行が自動的に処理に戻ることでした。したがって、ここでの私の質問は、それが可能かどうかです。
エリケ

破壊的な読み取りアプローチを試して、デキューされたアイテムを、必要に応じて再エンキューできる別のテーブルに配置します。それが修正されれば、この再エンキュープロセスがスムーズに機能するように投資することができます。
ニックチャマス

回答:


10

正確に 3つのロックヒントが必要です

  • 再作成
  • アップロック
  • ROWLOCK

私は以前にSOでこれに答えました:https : //stackoverflow.com/questions/939831/sql-server-process-queue-race-condition/940001#940001

Remusが言うように、Service Brokerを使用する方が良いですが、これらのヒントは機能します

分離レベルに関するエラーは通常、レプリケーションまたはNOLOCKが関係していることを意味します。


上記のスクリプトでこれらのヒントを使用すると、デッドロックとプロセスの順序が乱れます。(UPDATE SET ... FROM (SELECT TOP 1 ... FROM ... ORDER BY ...))これは、ロックを保持しているUPDATEパターンが機能しないことを意味しますか?また、あなたが結合する瞬間READPASTHOLDLOCKエラーが発生します。このサーバーにはレプリケーションがなく、分離レベルはREAD COMMITTEDです。
エリック

2
@ErikE-テーブルのクエリ方法と同じくらい重要なのは、テーブルの構造です。キューとして使用しているテーブルは、デキューされる次のアイテムが明確になるように、デキュー順序でクラスタ化する必要があります。これは重要です。上記のコードをざっと見てみると、クラスター化インデックスが定義されていません。
ニックチャマス

@Nickは完全に理にかなっており、なぜ私はそれを考えなかったのかわかりません。適切なPK制約を追加し(上記のスクリプトを更新しました)、それでもデッドロックが発生しました。ただし、アイテムは正しい順序で処理されるようになり、デッドロックされたアイテムの繰り返し処理は禁止されました。
エリック

@ErikE-1.キューには、キューに入れられたアイテムのみを含める必要があります。デキューとアイテムは、キューテーブルから削除することを意味します。代わりにを更新しStatusIDてアイテムをデキューしていることがわかります。あれは正しいですか?2.デキュー順序は明確でなければなりません。でアイテムをキューに入れる場合GETDATE()、大量の場合、複数のアイテムが同時にデキューに等しく適格になる可能性が非常に高くなります。これにより、デッドロックが発生します。IDENTITYクラスタ化インデックスにを追加して、明確なデキュー順序を保証することをお勧めします。
ニックチャマス

1

SQLサーバーは、リレーショナルデータの保存に最適です。ジョブキューに関しては、それほど素晴らしいものではありません。MySQL向けに書かれたこの記事を参照してくださいが、ここでも適用できます。https://blog.engineyard.com/2011/5-subtle-ways-youre-using-mysql-as-a-queue-and-why-itll-bite-you


ありがとう、エリック。質問に対する私の最初の返信で、私はSQL Server Service Brokerを使用することを提案していました。というのは、table-as-queueメソッドは実際にはデータベースの目的ではないことがわかっているからです。しかし、SBは本当にメッセージ専用であるため、これはもはや推奨事項ではありません。データベースに入れられたデータのACIDプロパティは、使用(試行)を試みるのに非常に魅力的なコンテナになります。汎用キューとして機能する代替の低コスト製品を提案できますか?また、バックアップなどができますか?
エリック

8
この記事は、キュー処理の既知の誤fallを犯しています:状態とイベントを1つのテーブルに結合します(実際、記事のコメントを見ると、これについては少し前に反対していました)。この問題の典型的な症状は、「処理済み/処理中」フィールドです。イベントと状態を組み合わせる(すなわち、状態テーブルを作る「キュー」)巨大なサイズに「キュー」を成長させる結果(状態テーブルがあるため であるキュー)。イベントを真のキューに分離すると、キューが「ドレイン」(空になる)になり、これははるかに優れた動作をます。
レムスルサヌ

この記事は、キューテーブルには作業の準備が整ったアイテムのみがあることを正確に示唆していませんか?
エリケ

2
@ErikE:この段落を参照していますか?また、1つの大きなテーブルの症候群を回避するのは非常に簡単です。新しい電子メール用に別のテーブルを作成するだけで、処理が完了したら、それらを長期ストレージに挿入してからキューテーブルから削除します。通常、新しい電子メールのテーブルは非常に小さくなり、その上での操作は高速になります。これに対する私の口論は、「大きなキュー」の問題の回避策として与えられていることです。この勧告は、記事のopenningしていたはずである根本的な問題。
レムスルサヌ

状態とイベントを明確に区別して考え始めると、vdownをはるかに簡単な方法で開始できます。でもrecomemendationは、上記に変化するであろうに新しい電子メールの挿入emailsテーブルnew_emailsキュー。処理はnew_emailsキューをポーリングし、emailsテーブルの状態を更新します。これにより、キュー内を移動する「脂肪」状態の問題も回避されます。分散処理と通信を使用した真のキュー(SSBなど)について話す場合、分散システムでは分散状態に問題があるため、事態はより複雑になります。
レムスルサヌ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.