私はSQLServerの単一のステートメントが一貫しているという仮定の下で操作してきました
その仮定は間違っています。次の2つのトランザクションには、同じロックセマンティクスがあります。
STATEMENT
BEGIN TRAN; STATEMENT; COMMIT
まったく違いはありません。単一のステートメントと自動コミットは何も変更しません。
したがって、すべてのロジックを1つのステートメントにマージしても効果はありません(マージする場合は、計画が変更されたために偶然でした)。
手元の問題を解決しましょう。 SERIALIZABLE
トランザクションがシングルスレッドで実行されたかのように動作することが保証されるため、表示されている不整合が修正されます。同様に、それらは即座に実行されたかのように動作します。
デッドロックが発生します。再試行ループに問題がない場合は、この時点で完了です。
より多くの時間を投資したい場合は、ロックのヒントを適用して、関連するデータへの排他的アクセスを強制します。
UPDATE Gifts -- U-locked anyway
SET GivenAway = 1
WHERE GiftID = (
SELECT TOP 1 GiftID
FROM Gifts WITH (UPDLOCK, HOLDLOCK) --this normally just S-locks.
WHERE g2.GivenAway = 0
AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
ORDER BY g2.GiftValue DESC
)
これで、同時実行性が低下します。負荷によっては、まったく問題ない場合があります。
問題の性質自体が、並行性の達成を困難にします。そのための解決策が必要な場合は、より侵襲的な手法を適用する必要があります。
UPDATEを少し簡略化できます:
WITH g AS (
SELECT TOP 1 Gifts.*
FROM Gifts
WHERE g2.GivenAway = 0
AND (SELECT COUNT(*) FROM Gifts g2 WITH (UPDLOCK, HOLDLOCK) WHERE g2.GivenAway = 1) < 5
ORDER BY g2.GiftValue DESC
)
UPDATE g -- U-locked anyway
SET GivenAway = 1
これにより、不要な結合が1つ削除されます。