Pracoval jsem za předpokladu, že jeden příkaz v SQL Server je konzistentní
Tento předpoklad je chybný. Následující dvě transakce mají identickou sémantiku zamykání:
STATEMENT
BEGIN TRAN; STATEMENT; COMMIT
Žádný rozdíl. Jednotlivé příkazy a automatické potvrzení nic nemění.
Takže sloučení veškeré logiky do jednoho prohlášení nepomůže (pokud ano, byla to náhoda, protože se změnil plán).
Pojďme problém vyřešit. SERIALIZABLE
opraví nekonzistenci, kterou vidíte, protože zaručuje, že se vaše transakce budou chovat, jako by byly prováděny jednovláknově. Ekvivalentně se chovají, jako by byly provedeny okamžitě.
Dostanete se do slepé uličky. Pokud jste v pořádku se smyčkou opakování, v tomto bodě jste hotovi.
Chcete-li investovat více času, použijte rady pro zamykání, abyste si vynutili výhradní přístup k relevantním datům:
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
)
Nyní uvidíte sníženou souběžnost. To může být zcela v pořádku v závislosti na vaší zátěži.
Samotná povaha vašeho problému ztěžuje dosažení souběžnosti. Pokud pro to potřebujete řešení, museli bychom použít invazivnější techniky.
UPDATE můžete trochu zjednodušit:
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
Tím se zbavíte jednoho zbytečného spojení.