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番目の質問:もっと良い方法はありますか?
このアプローチは正しいですか?
はい。それは質問で述べられたすべての目的を満たします。
戦略を説明し、関連する手順名をメモするための手順のコメントは、他のユーザーによる将来のメンテナンスに役立つ場合があります。
もっと良い方法はありますか?
私の心にはありません、いいえ。
単一のロックを取得することは非常に高速な操作であり、非常に明確なロジックになります。 2番目の手順でロックを取得することは冗長であることは私には明らかではありませんが、ロックされているとしても、それを省略することで実際に何が得られますか?あなたの実装のシンプルさと安全性は私に魅力的です。
代替案ははるかに複雑であり、本当にすべてのケースをカバーしたのか、または(おそらく微妙で明記されていない)仮定を破る将来の内部エンジンの詳細の変更があるのか疑問に思うかもしれません。
従来のキューイング実装が必要な場合は、次のリファレンスが非常に役立ちます。
このシナリオは、次の質問と非常によく似ています。
そこでの私の回答では、ここにあるものと同様のモデルを推奨しましたが、_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
_の使用は論理的に不要です。必要ない理由は次のとおりです。
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())
-この条件は、現在チェックアウトされているアイテムに当てはまります。次に、SetCheckComplete
はLastCheckCompleted
の値を変更します。次に、サーバーは2番目の条件_LastCheckStarted <= LastCheckCompleted
_をチェックし、それもtrueのように見えます。最終結果:20分待たずに、チェックインしたばかりの行をチェックアウトしました。
私の理解では、単一のオブジェクト(ヒープまたはインデックス)内ではこれは不可能ですが、複数のオブジェクト間ではimpossibleではありません。スキーマを見ると、LastCheckCompleted
にインデックスがあります。したがって、2つのこと:
LastCheckCompleted
を更新する必要があるため、このシナリオはほとんどあり得ないと思いますafter最初の条件が検証されましたが、テーブル(クラスター化インデックス)が最初に更新されてから、非クラスター化インデックスが更新されると思います、しかしこのシナリオを実現するには、NonClustered IndexからLastCheckCompleted
の値を取得する必要がありますよね?DATETIME
である新しいNULL
フィールドを追加できます。次に、20分前までにnew_fieldを確認します。NULL
(つまり、チェックアウト済み)の行はいずれにしても一致しません(誰かが_ANSI_NULLS OFF
_;-をオンにするのに十分な愚かでない限り)。