Ist eine Anweisung in SQL Server ACID
?
Was ich damit meine
Bei einer einzelnen T-SQL-Anweisung, die nicht in ein BEGIN TRANSACTION
/ eingeschlossen ist COMMIT TRANSACTION
, sind die Aktionen dieser Anweisung:
- Atomic : Entweder werden alle Datenänderungen durchgeführt oder keine.
- Konsistent : Nach Abschluss einer Transaktion müssen alle Daten in einem konsistenten Zustand bleiben.
- Isoliert : Änderungen, die durch gleichzeitige Transaktionen vorgenommen werden, müssen von den Änderungen isoliert werden, die durch andere gleichzeitige Transaktionen vorgenommen werden.
- Dauerhaft : Nachdem eine Transaktion abgeschlossen wurde, sind ihre Auswirkungen dauerhaft im System vorhanden.
Der Grund, den ich frage
Ich habe eine einzelne Anweisung in einem Live-System, die anscheinend gegen die Regeln der Abfrage verstößt.
Tatsächlich lautet meine T-SQL-Anweisung:
--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)
Hinweis : Eine einfachere konzeptionelle Variante könnte jedoch sein:
--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
)
Beachten Sie in diesen beiden Aussagen, dass es sich um einzelne Aussagen handelt (UPDATE...SET...WHERE
).
Es gibt Fälle, in denen die falsche Transaktion "gebucht" wird . es ist tatsächlich eine spätere Auswahl Transaktion ausgewählt. Nachdem ich 16 Stunden lang darauf gestarrt habe, bin ich ratlos. Es ist, als würde SQL Server einfach gegen die Regeln verstoßen.
Ich habe mich gefragt, was Slots
passiert , wenn sich die Ergebnisse der Ansicht ändern, bevor das Update durchgeführt wird. Was ist, wenn SQL Server SHARED
die Transaktionen an diesem Datum nicht sperrt ? ? Ist es möglich, dass eine einzelne Aussage inkonsistent sein kann?
Also habe ich beschlossen, es zu testen
Ich habe mich entschlossen zu prüfen, ob die Ergebnisse von Unterabfragen oder inneren Operationen inkonsistent sind. Ich habe eine einfache Tabelle mit einer einzelnen int
Spalte erstellt:
CREATE TABLE CountingNumbers (
Value int PRIMARY KEY NOT NULL
)
Aus mehreren Verbindungen heraus rufe ich in einer engen Schleife die einzelne T-SQL-Anweisung auf :
INSERT INTO CountingNumbers (Value)
SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
Mit anderen Worten lautet der Pseudocode:
while (true)
{
ADOConnection.Execute(sql);
}
Und innerhalb weniger Sekunden bekomme ich:
Violation of PRIMARY KEY constraint 'PK__Counting__07D9BBC343D61337'.
Cannot insert duplicate key in object 'dbo.CountingNumbers'.
The duplicate value is (1332)
Sind Aussagen atomar?
Die Tatsache, dass eine einzelne Aussage nicht atomar war, lässt mich fragen, ob einzelne Aussagen atomar sind.
Oder gibt es eine subtilere Definition von Anweisung , die sich (zum Beispiel) von der unterscheidet, die SQL Server als Anweisung betrachtet:
Bedeutet dies grundsätzlich, dass SQL Server-Anweisungen innerhalb der Grenzen einer einzelnen T-SQL-Anweisung nicht atomar sind?
Und wenn eine einzelne Aussage atomar ist, was erklärt die Schlüsselverletzung?
Aus einer gespeicherten Prozedur heraus
Anstatt dass ein Remote-Client n Verbindungen öffnet , habe ich es mit einer gespeicherten Prozedur versucht:
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'
und öffnete 5 Registerkarten in SSMS, drückte jeweils F5 und sah zu, wie auch sie gegen ACID verstießen:
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.
Der Fehler ist also unabhängig von ADO, ADO.net oder keinem der oben genannten.
Seit 15 Jahren gehe ich davon aus, dass eine einzelne Anweisung in SQL Server konsistent ist. und der einzige
Was ist mit TRANSACTION ISOLATION LEVEL xxx?
Für verschiedene Varianten des auszuführenden SQL-Batches:
Standard (Lese festgeschrieben) : Schlüsselverletzung
INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
Standard (Lese festgeschrieben), explizite Transaktion :
KeineVerletzung desFehlerschlüsselsBEGIN TRANSACTION INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers COMMIT TRANSACTION
serialisierbar : Deadlock
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
Snapshot (nach Änderung der Datenbank, um die Snapshot-Isolation zu aktivieren): Schlüsselverletzung
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
Bonus
- Microsoft SQL Server 2008 R2 (SP2) - 10.50.4000.0 (X64)
- Standardtransaktionsisolationsstufe (
READ COMMITTED
)
Es stellt sich heraus, dass jede Abfrage, die ich jemals geschrieben habe, fehlerhaft ist
Dies ändert sicherlich die Dinge. Jede Update-Anweisung, die ich jemals geschrieben habe, ist grundlegend kaputt. Z.B:
--Update the user with their last invoice date
UPDATE Users
SET LastInvoiceDate = (SELECT MAX(InvoiceDate) FROM Invoices WHERE Invoices.uid = Users.uid)
Falscher Wert; weil eine andere Rechnung nach dem MAX
und vor dem eingefügt werden könnte UPDATE
. Oder ein Beispiel von 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);
Ohne exklusive Holdlocks ist das SalesYTD
falsch.
Wie konnte ich all die Jahre etwas tun?
quelle
INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers
ist unter UNCOMMITTED, COMMITTED, REPEATABLE READ und SERIALIZABLE nicht sicher, daS
Sperren erforderlich sind . Und einS
Schloss ist mit einem anderenS
Schloss kompatibel . Andere gleichzeitige Anweisungen / Transaktionen können weiterhin dieselben Zeilen lesen. Um sicher zu gehen, benötigt dieser Ansatz eine SERIALIZABLE-Isolationsstufe plusX
Sperren oderX
Sperren + HOLDLOCK-Tabellenhinweise :INSERT INTO CountingNumbers (Value) SELECT ISNULL(MAX(Value), 0)+1 FROM CountingNumbers WITH(XLOCK, HOLDLOCK)
.Antworten:
Diese Annahme ist falsch. Die folgenden zwei Transaktionen haben eine identische Sperrsemantik:
STATEMENT BEGIN TRAN; STATEMENT; COMMIT
Kein Unterschied. Einzelne Anweisungen und automatische Festschreibungen ändern nichts.
Das Zusammenführen aller Logik zu einer Anweisung hilft also nicht (wenn ja, war es ein Zufall, weil sich der Plan geändert hat).
Lassen Sie uns das Problem beheben.
SERIALIZABLE
behebt die Inkonsistenz, die angezeigt wird, da dadurch sichergestellt wird, dass sich Ihre Transaktionen so verhalten, als würden sie einzeln ausgeführt. Entsprechend verhalten sie sich so, als würden sie sofort ausgeführt.Sie werden Deadlocks bekommen. Wenn Sie mit einer Wiederholungsschleife einverstanden sind, sind Sie an diesem Punkt fertig.
Wenn Sie mehr Zeit investieren möchten, wenden Sie Sperrhinweise an, um den exklusiven Zugriff auf die relevanten Daten zu erzwingen:
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 )
Sie sehen jetzt eine reduzierte Parallelität. Das kann je nach Belastung völlig in Ordnung sein.
Die Natur Ihres Problems macht es schwierig, Parallelität zu erreichen. Wenn Sie eine Lösung dafür benötigen, müssen wir invasivere Techniken anwenden.
Sie können das UPDATE etwas vereinfachen:
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
Dadurch wird eine unnötige Verknüpfung beseitigt.
quelle
Unten finden Sie ein Beispiel für eine UPDATE-Anweisung, die einen Zählerwert atomar erhöht
-- 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
quelle