web-dev-qa-db-ja.com

sp_getapplockを使用してキューを実装します。それが正しいか?もっと良い方法はありますか?

SQL Server分離レベル に関するPaul Whiteの一連の投稿を読んでいて、 phrase に出くわしました:

ポイントを強調するために、T-SQLで記述された疑似制約は、どのような同時変更が発生している場合でも、正しく実行する必要があります。アプリケーション開発者は、lockステートメントを使用して、そのような機密性の高い操作を保護する場合があります。 T-SQLプログラマーがリスクのあるストアドプロシージャとトリガーコードの機能に最も近いものは、比較的まれに使用されるsp_getapplockシステムストアドプロシージャです。それは、それが存在し、状況によっては正しい選択になり得るということだけが、または優先されるオプションでさえあると言っているのではありません。

私はsp_getapplockを使用していて、これを正しく使用しているかどうか、または目的の効果を得るためのより良い方法があるかどうか疑問に思いました。

いわゆる「構築サーバー」を24時間365日ループで処理するC++アプリケーションがあります。これらの構築サーバーのリストを含むテーブルがあります(約200行)。新しい行はいつでも追加できますが、頻繁には追加されません。行は削除されませんが、非アクティブとしてマークすることができます。サーバーの処理には数秒から数十分かかることがあります。各サーバーは異なり、「小さい」ものもあれば、「大きい」ものもあります。サーバーが処理されると、アプリケーションはそれを再び処理する前に少なくとも20分間待機する必要があります(サーバーを頻繁にポーリングしないでください)。アプリケーションは、並列処理を実行する10個のスレッドを開始しますが、私は、2つのスレッドが同時に同じサーバーを処理しようとしないことを保証する必要があります。 2つの異なるサーバーを同時に処理することができ、同時に処理する必要がありますが、各サーバーは20分に1回以下で処理できます。

以下はテーブルの定義です:

CREATE TABLE [dbo].[PortalBuildingServers](
    [InternalIP] [varchar](64) NOT NULL,
    [LastCheckStarted] [datetime] NOT NULL,
    [LastCheckCompleted] [datetime] NOT NULL,
    [IsActiveAndNotDisabled] [bit] NOT NULL,
    [MaxBSMonitoringEventLogItemID] [bigint] NOT NULL,
CONSTRAINT [PK_PortalBuildingServers] PRIMARY KEY CLUSTERED 
(
    [InternalIP] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_LastCheckCompleted] ON [dbo].[PortalBuildingServers]
(
    [LastCheckCompleted] ASC
)
INCLUDE 
(
    [LastCheckStarted],
    [IsActiveAndNotDisabled],
    [MaxBSMonitoringEventLogItemID]
) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]

アプリケーションのワーカースレッドのメインループは次のようになります。

for(;;)
{
    // Choose building server for checking
    std::vector<SBuildingServer> vecBS = GetNextBSToCheck();
    if (vecBS.size() == 1)
    {
        // do the check and don't go to sleep afterwards
        SBuildingServer & bs = vecBS[0];
        DoCheck(bs);
        SetCheckComplete(bs);
    }
    else
    {
        // Sleep for a while
        ...
    }
}

ここでの2つの関数GetNextBSToCheckおよびSetCheckCompleteは、対応するストアドプロシージャを呼び出しています。

GetNextBSToCheckは0または1行を返し、次に処理するサーバーの詳細を示します。これは、最も長い間処理されていないサーバーです。この「最も古い」サーバーが処理されてから20分未満の場合、行は返されず、スレッドは1分間待機します。

SetCheckCompleteは処理が完了した時刻を設定するため、20分後にこのサーバーを処理用に再度選択できます。

最後に、ストアドプロシージャのコード:

GetNextToCheck

CREATE PROCEDURE [dbo].[GetNextToCheck]
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;
    BEGIN TRY
        DECLARE @VarInternalIP varchar(64) = NULL;
        DECLARE @VarMaxBSMonitoringEventLogItemID bigint = NULL;

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'PortalBSChecking_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            -- Find BS that wasn't checked for the longest period
            SELECT TOP 1
                @VarInternalIP = InternalIP
                ,@VarMaxBSMonitoringEventLogItemID = MaxBSMonitoringEventLogItemID
            FROM
                dbo.PortalBuildingServers
            WHERE
                LastCheckStarted <= LastCheckCompleted
                -- this BS is not being checked right now
                AND LastCheckCompleted < DATEADD(minute, -20, GETDATE())
                -- last check was done more than 20 minutes ago
                AND IsActiveAndNotDisabled = 1
            ORDER BY LastCheckCompleted
            ;

            -- Start checking the found BS
            UPDATE dbo.PortalBuildingServers
            SET LastCheckStarted = GETDATE()
            WHERE InternalIP = @VarInternalIP;
            -- There is no need to explicitly verify if we found anything.
            -- If @VarInternalIP is null, no rows will be updated
        END;

        -- Return found BS, 
        -- or no rows if nothing was found, or failed to acquire the lock
        SELECT
            @VarInternalIP AS InternalIP
            ,@VarMaxBSMonitoringEventLogItemID AS MaxBSMonitoringEventLogItemID
        WHERE
            @VarInternalIP IS NOT NULL
            AND @VarMaxBSMonitoringEventLogItemID IS NOT NULL
        ;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH;

END

SetCheckComplete

CREATE PROCEDURE [dbo].[SetCheckComplete]
    @ParamInternalIP varchar(64)
AS
BEGIN
    SET NOCOUNT ON;

    BEGIN TRANSACTION;
    BEGIN TRY

        DECLARE @VarLockResult int;
        EXEC @VarLockResult = sp_getapplock
            @Resource = 'PortalBSChecking_app_lock',
            @LockMode = 'Exclusive',
            @LockOwner = 'Transaction',
            @LockTimeout = 60000,
            @DbPrincipal = 'public';

        IF @VarLockResult >= 0
        BEGIN
            -- Acquired the lock
            -- Completed checking the given BS
            UPDATE dbo.PortalBuildingServers
            SET LastCheckCompleted = GETDATE()
            WHERE InternalIP = @ParamInternalIP;
        END;

        COMMIT TRANSACTION;
    END TRY
    BEGIN CATCH
        ROLLBACK TRANSACTION;
    END CATCH;

END

ご覧のとおり、私はsp_getapplockを使用して、これらの両方のストアドプロシージャのインスタンスが常に1つだけ実行されることを保証しています。 「最も古い」サーバーを選択するクエリはLastCheckCompleted時間を使用するため、両方の手順でsp_getapplockを使用する必要があると思います。これはSetCheckCompleteによって更新されます。

このコードは、2つのスレッドが同時に同じサーバーを処理しようとしないことを保証していると思いますが、このコードと全体的なアプローチに関する問題を指摘していただければ幸いです。それで、最初の質問:このアプローチは正しいですか?

また、sp_getapplockを使用しなくても同じ効果が得られるかどうかを知りたいです。 2番目の質問:もっと良い方法はありますか?

5

このアプローチは正しいですか?

はい。それは質問で述べられたすべての目的を満たします。

戦略を説明し、関連する手順名をメモするための手順のコメントは、他のユーザーによる将来のメンテナンスに役立つ場合があります。

もっと良い方法はありますか?

私の心にはありません、いいえ。

単一のロックを取得することは非常に高速な操作であり、非常に明確なロジックになります。 2番目の手順でロックを取得することは冗長であることは私には明らかではありませんが、ロックされているとしても、それを省略することで実際に何が得られますか?あなたの実装のシンプルさと安全性は私に魅力的です。

代替案ははるかに複雑であり、本当にすべてのケースをカバーしたのか、または(おそらく微妙で明記されていない)仮定を破る将来の内部エンジンの詳細の変更があるのか​​疑問に思うかもしれません。


従来のキューイング実装が必要な場合は、次のリファレンスが非常に役立ちます。

キューとしてのテーブルの使用Remus Rusan による

4
Paul White 9

このシナリオは、次の質問と非常によく似ています。

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

そこでの私の回答では、ここにあるものと同様のモデルを推奨しましたが、_sp_applock_をフェイルセーフとして含めるという概念は、最初のコンセプトが完全なものではなかった場合に限られます。

「チェックアウト」プロセスの主な違いは、CTEとSELECT句を使用してUPDATEクエリとOUTPUTクエリを組み合わせたことです。これにより、SELECTに対する_(READPAST, ROWLOCK, UPDLOCK)_の適切なクエリヒントにより、行が処理に適格かどうかを判断するために使用されるフィールドを更新すると同時に、その値を返し、呼び出しプロセスに返すことができます。 。これら2つのステップを組み合わせると、アプリのロックなしですべきでも問題ありません。そして、現在のモデルで「チェックアウト」プロセスを実行している個々のスレッドが(質問に投稿されているように)残りの9つのスレッドを待機させるため、アプリロックを取り除くと、スループットが向上します。ほぼ同時に並んでいる次のものをつかむ。

質問の終わりに向けた次のステートメントについて:

私は_sp_getapplock_を使用して、これらの両方のストアドプロシージャのインスタンスが常に1つだけ実行されることを保証しています。 「最も古い」サーバーを選択するクエリは、LastCheckCompletedによって更新されるSetCheckComplete時間を使用するため、両方の手順で_sp_getapplock_を使用する必要があると思います。

現在のアプローチを維持するか、「CTE + OUTPUT句を介したSELECT + UPDATEの組み合わせ」(tm)アプローチに切り替えるかにかかわらず、SetCheckCompleteストアドプロシージャでの_sp_getapplock_の使用は論理的に不要です。必要ない理由は次のとおりです。

  • 一度に1つのスレッドだけがレコードをチェックアウトできます
  • SetCheckCompleteでアプリロックを使用することは、LastCheckCompletedの値がGetNextToCheckに決定的な影響を与える可能性があることを意味します。
    • レコードがチェックインされるまで、LastCheckStartedフィールドはLastCheckCompletedになり、この状態により、_LastCheckStarted <= LastCheckCompleted_条件により、レコードは "GetNext"クエリから除外されます
    • チェックインされると、_LastCheckStarted <= LastCheckCompleted_はレコードをフィルターで除外しなくなりますが、LastCheckCompleted < DATEADD(minute, -20, GETDATE())条件は、定義により、このクエリの実行前に数ミリ秒で完了したため、レコードをフィルターで除外します。

したがって、SetCheckCompleteは、GetNextToCheckプロセスから完全に独立しています。何らかの量のセーフガードを追加する必要があるのは、GetNextToCheckプロセスだけです。

SetCheckCompleteからアプリロックを削除すると、完全に安全であるだけでなく、任意のロック_@Resource_の競合が減少するため、スループットも向上します(ここでも、現在のモデルを維持するか、または推奨)。


[〜#〜]更新[〜#〜]

この回答のコメントからの質問:

GetNextクエリのWHEREには2つの条件があります。サーバーが最初に1つの条件をチェックすることは可能ですか?LastCheckCompleted < DATEADD(minute, -20, GETDATE())-この条件は、現在チェックアウトされているアイテムに当てはまります。次に、SetCheckCompleteLastCheckCompletedの値を変更します。次に、サーバーは2番目の条件_LastCheckStarted <= LastCheckCompleted_をチェックし、それもtrueのように見えます。最終結果:20分待たずに、チェックインしたばかりの行をチェックアウトしました。

私の理解では、単一のオブジェクト(ヒープまたはインデックス)内ではこれは不可能ですが、複数のオブジェクト間ではimpossibleではありません。スキーマを見ると、LastCheckCompletedにインデックスがあります。したがって、2つのこと:

  1. LastCheckCompletedを更新する必要があるため、このシナリオはほとんどあり得ないと思いますafter最初の条件が検証されましたが、テーブル(クラスター化インデックス)が最初に更新されてから、非クラスター化インデックスが更新されると思います、しかしこのシナリオを実現するには、NonClustered IndexからLastCheckCompletedの値を取得する必要がありますよね?
  2. どちらの方法でも、(より高いレベルのトランザクション分離レベルを使用する以外に)この可能性を許可しない簡単な修正は、単一のステータスフィールドを作成することです。少なくとも少し複雑なことは、追加の情報が必要なことです(つまり、資格は「チェックアウトされていない」と「19分以上前にチェックインされている」で構成されます)。おそらく、現在の2つの時刻フィールドを情報として保持し、「チェックアウト」またはチェックイン時刻のDATETIMEである新しいNULLフィールドを追加できます。次に、20分前までにnew_fieldを確認します。NULL(つまり、チェックアウト済み)の行はいずれにしても一致しません(誰かが_ANSI_NULLS OFF_;-をオンにするのに十分な愚かでない限り)。
1
Solomon Rutzky