Une instruction dans SQL Server est-elle ACID
?
Étant donné une seule instruction T-SQL, pas enveloppé dans un BEGIN TRANSACTION
/COMMIT TRANSACTION
, sont les actions de cette déclaration:
J'ai une seule déclaration dans un système en direct qui semble violer les règles de la requête.
En effet, ma déclaration T-SQL est:
--If there are any slots available,
--then find the earliest unbooked transaction and mark it booked
UPDATE Transactions
SET Booked = 1
WHERE TransactionID = (
SELECT TOP 1 TransactionID
FROM Slots
INNER JOIN Transactions t2
ON Slots.SlotDate = t2.TransactionDate
WHERE t2.Booked = 0 --only book it if it's currently unbooked
AND Slots.Available > 0 --only book it if there's empty slots
ORDER BY t2.CreatedDate)
Note: Mais une variante conceptuelle plus simple pourrait être:
--Give away one gift, as long as we haven't given away five
UPDATE Gifts
SET GivenAway = 1
WHERE GiftID = (
SELECT TOP 1 GiftID
FROM Gifts
WHERE g2.GivenAway = 0
AND (SELECT COUNT(*) FROM Gifts g2 WHERE g2.GivenAway = 1) < 5
ORDER BY g2.GiftValue DESC
)
Dans ces deux instructions, notez qu'il s'agit d'instructions uniques (UPDATE...SET...WHERE
).
Il y a des cas où la mauvaise transaction est "réservée" ; il s'agit en fait de sélectionner une transaction ultérieure . Après avoir regardé ça pendant 16 heures, je suis perplexe. C'est comme si SQL Server violait simplement les règles.
Je me demandais si les résultats de la vue Slots
changeaient avant la mise à jour? Que faire si SQL Server ne détient pas les verrous SHARED
sur les transactions à ce date? Est-il possible qu'une seule déclaration puisse être incohérente?
J'ai décidé de vérifier si les résultats des sous-requêtes ou des opérations internes sont incohérents. J'ai créé une table simple avec une seule colonne int
:
CREATE TABLE CountingNumbers (
Value int PRIMARY KEY NOT NULL
)
À partir de plusieurs connexions, dans une boucle étroite, j'appelle la instruction T-SQL unique:
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
En d'autres termes, le pseudo-code est:
while (true)
{
ADOConnection.Execute(sql);
}
Et en quelques secondes, je reçois:
Violation of PRIMARY KEY constraint 'PK__Counting__07D9BBC343D61337'.
Cannot insert duplicate key in object 'dbo.CountingNumbers'.
The duplicate value is (1332)
Le fait qu'une seule déclaration n'était pas atomique me fait me demander si des déclarations uniques sont atomiques?
Ou existe-t-il une définition plus subtile de l'instruction , qui diffère de ( par exemple) ce que SQL Server considère comme une instruction:
Cela signifie-t-il fondamentalement que, dans le cadre d'une seule instruction T-SQL, les instructions SQL Server ne sont pas atomiques?
Et si une seule déclaration est atomique, qu'est-ce qui explique la violation clé?
Plutôt qu'un client distant ouvrant n connexions, je l'ai essayé avec une procédure stockée:
CREATE procedure [dbo].[DoCountNumbers] AS
SET NOCOUNT ON;
DECLARE @bumpedCount int
SET @bumpedCount = 0
WHILE (@bumpedCount < 500) --safety Valve
BEGIN
SET @bumpedCount = @bumpedCount+1;
PRINT 'Running bump '+CAST(@bumpedCount AS varchar(50))
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
IF (@bumpedCount >= 500)
BEGIN
PRINT 'WARNING: Bumping safety limit of 500 bumps reached'
END
END
PRINT 'Done bumping process'
et a ouvert 5 onglets dans SSMS, a appuyé sur F5 dans chacun, et a regardé comme ils violaient aussi ACID:
Running bump 414
Msg 2627, Level 14, State 1, Procedure DoCountNumbers, Line 14
Violation of PRIMARY KEY constraint 'PK_CountingNumbers'.
Cannot insert duplicate key in object 'dbo.CountingNumbers'.
The duplicate key value is (4414).
The statement has been terminated.
L'échec est donc indépendant d'ADO, d'ADO.net ou de rien de ce qui précède.
Depuis 15 ans, j'opère sous l'hypothèse qu'une seule instruction dans SQL Server est cohérente; et le seul
Pour que différentes variantes du batch SQL s'exécutent:
par défaut (lecture validée): violation de clé
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
par défaut (lecture validée), transaction explicite: pas d'erreur violation de clé
BEGIN TRANSACTION
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
COMMIT TRANSACTION
sérialisable: blocage
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
COMMIT TRANSACTION
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
instantané (après avoir modifié la base de données pour activer l'isolement de l'instantané): violation de clé
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
BEGIN TRANSACTION
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
COMMIT TRANSACTION
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
READ COMMITTED
)Cela change certainement les choses. Chaque déclaration de mise à jour que j'ai jamais écrite est fondamentalement cassée. Par exemple.:
--Update the user with their last invoice date
UPDATE Users
SET LastInvoiceDate = (SELECT MAX(InvoiceDate) FROM Invoices WHERE Invoices.uid = Users.uid)
Mauvaise valeur; car une autre facture pourrait être insérée après le MAX
et avant le UPDATE
. Ou un exemple de BOL:
UPDATE Sales.SalesPerson
SET SalesYTD = SalesYTD +
(SELECT SUM(so.SubTotal)
FROM Sales.SalesOrderHeader AS so
WHERE so.OrderDate = (SELECT MAX(OrderDate)
FROM Sales.SalesOrderHeader AS so2
WHERE so2.SalesPersonID = so.SalesPersonID)
AND Sales.SalesPerson.BusinessEntityID = so.SalesPersonID
GROUP BY so.SalesPersonID);
sans verrous exclusifs, le SalesYTD
est faux.
Comment ai-je pu faire quoi que ce soit pendant toutes ces années.
J'ai fonctionné sous l'hypothèse qu'une seule instruction dans SQL Server est cohérente
Cette hypothèse est fausse. Les deux transactions suivantes ont une sémantique de verrouillage identique:
STATEMENT
BEGIN TRAN; STATEMENT; COMMIT
Aucune différence. Les instructions simples et les validations automatiques ne changent rien.
Donc, fusionner toute la logique en une seule instruction n'aide pas (si c'est le cas, c'était par accident parce que le plan a changé).
Corrigeons le problème à portée de main. SERIALIZABLE
corrigera l'incohérence que vous voyez, car elle garantit que vos transactions se comportent comme si elles étaient exécutées de façon unique. De manière équivalente, ils se comportent comme s'ils s'exécutaient instantanément.
Vous obtiendrez des blocages. Si vous êtes d'accord avec une boucle de nouvelle tentative, vous avez terminé à ce stade.
Si vous souhaitez investir plus de temps, appliquez des conseils de verrouillage pour forcer l'accès exclusif aux données pertinentes:
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
)
Vous verrez maintenant une simultanéité réduite. Cela pourrait être tout à fait correct en fonction de votre charge.
La nature même de votre problème rend difficile l'accès simultané. Si vous avez besoin d'une solution pour cela, nous aurions besoin d'appliquer des techniques plus invasives.
Vous pouvez simplifier un peu la MISE À JOUR:
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
Cela supprime une jointure inutile.
Ci-dessous est un exemple d'une instruction UPDATE qui incrémente atomiquement une valeur de compteur
-- Do this once for test setup
CREATE TABLE CountingNumbers (Value int PRIMARY KEY NOT NULL)
INSERT INTO CountingNumbers VALUES(1)
-- Run this in parallel: start it in two tabs on SQL Server Management Studio
-- You will see each connection generating new numbers without duplicates and without timeouts
while (1=1)
BEGIN
declare @nextNumber int
-- Taking the Update lock is only relevant in case this statement is part of a larger transaction
-- to prevent deadlock
-- When executing without a transaction, the statement will itself be atomic
UPDATE CountingNumbers WITH (UPDLOCK, ROWLOCK) SET @nextNumber=Value=Value+1
print @nextNumber
END