SQL-Server-Transaktionsfehler korrekt zurückrollen
Verbinden Sie TRY CATCH, XACT_ABORT und XACT_STATE mit klarer Transaktionsverantwortung, damit Teiländerungen und falsche Erfolgsmeldungen ausbleiben.
Wenn ein Bestellvorgang den Lagerbestand reduziert, aber keine Reservierung anlegen kann, ist er nicht teilweise erfolgreich. Er verletzt eine fachliche Regel. SQL Server kann beide Änderungen atomar ausführen, sofern Fehler und Transaktionsgrenzen konsequent behandelt werden. Ein CATCH, das nur eine Meldung ausgibt und normal zurückkehrt, kann dabei einen gefährlichen Scheinerfolg erzeugen.
Definieren Sie zuerst den Erfolg. Bestandsreduktion und Reservierung müssen gemeinsam bestätigt oder gemeinsam verworfen werden. Das Beispiel besitzt seine Transaktion selbst und lehnt eine bereits offene äußere Transaktion ab. Wiederverwendbare Prozeduren innerhalb größerer Abläufe benötigen einen anderen, ausdrücklich vereinbarten Vertrag.
Nach der ersten Änderung absichtlich scheitern
Erstellen Sie diese temporären Tabellen in einer Übungsverbindung.
CREATE TABLE #Stock (ProductId int PRIMARY KEY, Quantity int NOT NULL);
CREATE TABLE #Reservations (
ReservationId int PRIMARY KEY, ProductId int NOT NULL, Quantity int NOT NULL
);
INSERT #Stock VALUES (1, 10);
INSERT #Reservations VALUES (1, 1, 1);
Die Reservierungskennung ist bereits vorhanden. Der nächste Batch reduziert den Bestand und löst danach absichtlich einen Eindeutigkeitsfehler aus.
SET XACT_ABORT ON;
IF @@TRANCOUNT <> 0
THROW 50001, 'Run without an existing transaction.', 1;
DECLARE @ReservationId int = 1; -- Deliberate duplicate for the failure test.
DECLARE @Quantity int = 3;
BEGIN TRY
BEGIN TRANSACTION;
UPDATE #Stock
SET Quantity = Quantity - @Quantity
WHERE ProductId = 1 AND Quantity >= @Quantity;
IF @@ROWCOUNT <> 1
THROW 50002, 'Insufficient stock or missing product.', 1;
INSERT #Reservations(ReservationId, ProductId, Quantity)
VALUES (@ReservationId, 1, @Quantity);
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW;
END CATCH;
Das bedingte UPDATE prüft und verändert die Verfügbarkeit in einer Anweisung. Eine getrennte ungeschützte Bestandsabfrage könnte konkurrierende Zugriffe mit veralteten Werten arbeiten lassen. Produktiver Code muss zusätzlich Mengen kleiner oder gleich null ablehnen. Der feste positive Beispielwert konzentriert den Versuch auf die Transaktion.
Weil INSERT fehlschlägt, muss auch die vorherige Reduktion verschwinden. Führen Sie nach dem erwarteten Fehler diese Prüfung separat in derselben Verbindung aus.
-- Run separately after the expected error, in the same connection.
SELECT ProductId, Quantity FROM #Stock;
SELECT @@TRANCOUNT AS OpenTransactions, XACT_STATE() AS TransactionState;
-- Expected: Quantity = 10, OpenTransactions = 0, TransactionState = 0.
Der Bestand sollte weiterhin 10 sein; eine offene Transaktion darf nicht bestehen. Für den Erfolgsfall starten Sie mit frischen Übungsdaten und einer neuen Reservierungskennung, etwa 2. Dann sollten 7 Einheiten verbleiben. Nur die Fehlermeldung zu prüfen würde übersehen, ob die erste Änderung erhalten blieb.
Drei Mechanismen unterscheiden
TRY CATCH übernimmt viele Ausführungsfehler, aber nicht jede denkbare Störung. Bestimmte Kompilierungsfehler derselben Ebene, Verbindungsabbrüche und Abbrüche durch den Client brauchen auch Behandlung außerhalb der Prozedur. Verschwindet die Verbindung selbst, kann die Prozedur keine normale Rückmeldung mehr garantieren.
SET XACT_ABORT ON sorgt bei vielen Laufzeitfehlern für den Abbruch der Transaktion statt nur einer einzelnen Anweisung. Für atomare Schreibvorgänge ist das nützlich, ersetzt aber weder Aufräumen noch Fehlerweitergabe. THROW berücksichtigt XACT_ABORT. RAISERROR verhält sich anders und ist kein beliebig austauschbarer Ersatz.
XACT_STATE unterscheidet fehlende, bestätigbare und nicht mehr bestätigbare Transaktionen. @@TRANCOUNT zeigt nur die Verschachtelungstiefe. In diesem selbst verantworteten Muster wird jede verbleibende Transaktion zurückgerollt, weil der gesamte Fachvorgang gescheitert ist, selbst wenn COMMIT technisch noch möglich wäre.
Das alleinstehende THROW im CATCH erhält die ursprünglichen Fehlerinformationen. Ein pauschaler Erfolgswert oder eine neue allgemeine Fehlernummer erschwert Diagnose und Wiederholungsentscheidungen. Für Telemetrie können ERROR_NUMBER, ERROR_PROCEDURE und ERROR_LINE zusammen mit einer Anfragekennung erfasst werden.
Den Vertrag mit dem Aufrufer einhalten
Ein einfaches ROLLBACK ohne Sicherungspunkt verwirft die gesamte Transaktion einschließlich vorheriger Arbeit des Aufrufers. Kopieren Sie dieses Muster deshalb nicht unverändert in verschachtelte Hilfsprozeduren. Innere BEGIN TRANSACTION und COMMIT erzeugen keine eigenständig dauerhaften Teiltransaktionen.
Eine zusammensetzbare Prozedur kann die anfängliche Transaktionsanzahl erfassen und gegebenenfalls einen Sicherungspunkt verwenden. Eine nicht bestätigbare Transaktion lässt sich dadurch aber nicht reparieren; ihr Besitzer muss vollständig zurückrollen. Auch verteilte Transaktionen begrenzen den Einsatz von Sicherungspunkten. Eine einfache eindeutige Verantwortung ist oft besser als ein scheinbar universeller Handler.
Dauerhafte Fehlerprotokollierung gehört hinter das Rollback oder in einen unabhängigen Kanal. Innerhalb einer nicht bestätigbaren Transaktion kann auch die Protokollierung scheitern; innerhalb der zurückgerollten Transaktion verschwindet sie wieder. Sensible Parameter sollten nicht unnötig gespeichert werden.
Ein Netzwerk-Timeout während COMMIT beweist schließlich keinen Rollback. Nutzen Sie Anfragekennungen und maßgebliche Statusprüfungen für nicht gefahrlos wiederholbare Vorgänge. Prüfen Sie Fehler nach jeder Änderung, den Erfolgsfall und die Antwort an den Aufrufer.
Technische Referenzen: Microsoft Learn: SET XACT_ABORT · Microsoft Learn: TRY CATCH · Microsoft Learn: XACT_STATE.