Diese Seite wurde maschinell übersetzt. Lesen Sie das englische Original. English

Transaktionen in Firebird: ACID, Isolationsstufen, Deadlocks und die Lösung von Update-Konflikten

Alexey Kovyazin, mit Unterstützung von Vlad Khorsun und Dmitry Kuzmenko, 08-APR-2019

Inhalt:

Ist es notwendig zu wissen, wie Transaktionen funktionieren?

Wahrscheinlich schon, denn weil der Begriff der Transaktionen einfach ist, unterschätzen viele Entwickler die Bedeutung der korrekten Verwendung von Transaktionen in Firebird. Erst nachdem Sie jedoch ein gründliches Verständnis dafür gewonnen haben, wie Transaktionen funktionieren, können Sie viele mysteriöse Dinge im Zusammenhang mit der Leistung verstehen, wie z. B. plötzliche Datenbankverlangsamungen (verbunden mit dem Aufräumen übermäßiger Datensatzversionen, die durch schlechtes Transaktionsmanagement entstehen).

Im Allgemeinen wird der Begriff einer Transaktion auf jedes dynamische System angewendet, das von einem Zustand in einen anderen übergeht. Ein klassisches Transaktionsbeispiel ist die Überweisung von Geld von einem Konto auf ein anderes. Normalerweise sieht das ungefähr so aus:

Code

Begin   --- Geld von Konto 1 auf Konto 2 überweisen
 --Konto 1 verringern
 --Konto 2 erhöhen
End - Transaktion committen

Das Beispiel läuft darauf hinaus, dass das Geld gleichzeitig von Konto 1 verschwinden und auf Konto 2 erscheinen muss, andernfalls gibt es entweder einen Geldüberschuss oder einen unerklärlichen Geldmangel für einige Zeit im System.

Aus Sicht der Datenbanken wird eine Transaktion normalerweise als eine Gruppe von Operationen definiert, die auf einer Datenbank ausgeführt werden und als unabhängig von anderen Transaktionen betrachtet wird. Meiner Meinung nach ist diese Definition weder besser noch schlechter als andere Definitionen, aber wie jede Definition macht sie wenig Sinn, ohne die tatsächlichen internen Abläufe und die Logik eines DBMS zu kennen.

Es wird angenommen, dass eine Transaktion in einer Datenbank die sogenannten ACID-Anforderungen erfüllen muss

Code
A - Atomarität
С - Konsistenz
I - Isolation
D - Dauerhaftigkeit

Viele Entwickler von Datenbankanwendungen sind von diesem Akronym so inspiriert, dass sie oft Argumente wie “Sie haben kein D in ACID” verwenden, wenn es um den Vergleich verschiedener DBMS geht (was normalerweise sofort gefolgt wird von “Es ist mir egal, was Sie denken”).

Eigentlich ist alles ziemlich einfach - ACID ist eine Reihe von Anforderungen an die Implementierung einer Transaktion in einem bestimmten DBMS, einige davon sind sehr streng (z. B. D - natürlich ist Dauerhaftigkeit wichtig!), während andere weniger streng sind - wenn wir uns die Transaktionsisolationsstufen ansehen, werden wir sehen, dass die Isolation variieren kann.

Deshalb lohnt es sich nicht, sofort zu versuchen, die wörtliche Bedeutung dieses Akronyms zu verstehen. Stattdessen werden wir die Logik untersuchen, wie DBMS (und insbesondere Transaktionen) funktionieren, und ACID aus der Perspektive “wie es gemacht ist” betrachten, anstatt “was es bedeutet”.

Da Transaktionsaspekte komplex sind, benötigen wir eine grafische Darstellung - eine Art Diagramme, um die Arbeit und Interaktion von Transaktionen zu zeigen. Mit diesen Diagrammen können wir eine logische Erzählung aufbauen und uns die Details ansehen, wie Transaktionen funktionieren.

Zunächst führen wir eine Zeitachse ein, weil sich Transaktionen in der Zeit entwickeln. Die Zeitachse wird so markiert, wie wir sie benötigen - wir brauchen weder Sekunden noch Minuten, sondern die wichtigsten Schritte der Interaktion zwischen Transaktionen:

Dann fügen wir eine Transaktion auf dieser Zeitachse hinzu - zeichnen wir sie in Form eines Rechtecks, dessen Seiten dem Beginn und Ende der Transaktion entsprechen. Da alle Transaktionen in Firebird nummeriert sind, geben wir auch die Transaktionsnummer an.

Das Diagramm zeigt also Transaktion Nummer 11, die zum Zeitpunkt t3 begann und zum Zeitpunkt t10 endete. Es gibt zwei Möglichkeiten, wie eine Transaktion enden kann - COMMIT, d. h. alle innerhalb der Transaktion vorgenommenen Änderungen anwenden, und ROLLBACK, d. h. alle innerhalb der Transaktion vorgenommenen Änderungen abbrechen. Wir zeigen die Art und Weise, wie eine Transaktion endet, folgendermaßen:

Um fortfahren zu können, müssen wir verschiedene Parameter von Transaktionen in diesen Diagrammen angeben, und wir geben sie in der unteren linken Ecke des Rechtecks an, das die entsprechende Transaktion darstellt - dieses Beispiel zeigt, dass Transaktion #11 die Isolationsstufe Snapshot hat.

Wenn wir sagen, dass “Transaktion X Daten einfügt” oder “Transaktion Y diese und jene Daten liest” - das ist formal falsch, weil wir sagen sollten “Änderungen wurden innerhalb von Transaktion X vorgenommen”. Nur SQL-Anweisungen können Daten lesen oder einfügen. Wenn es also für die Erzählung wichtig ist, zeigen wir diese Anweisungen innerhalb des Transaktionsrechtecks:

In diesem Beispiel haben wir die INSERT-Operation für Tabelle T1, Feld i1, Wert 100 - diese Operation wird innerhalb von Transaktion #11 ausgeführt und committet.

Außerdem müssen wir manchmal das Ergebnis einer Operation zeigen, zum Beispiel im folgenden Beispiel:

Dieses Beispiel zeigt Folgendes:

  1. Transaktion #11 mit dem Isolationsstufenparameter Snapshot (Isolationsstufen werden später besprochen, hier wird sie nur gezeigt, um das vollständige Bild zu zeichnen) wird zum Zeitpunkt t3 gestartet
  2. Die Operation INSERT INTO T1(i1) values (100), die Wert 100 in Feld i1 von Tabelle t1 einfügt, beginnt zum Zeitpunkt t5 UND endet zum Zeitpunkt t7
  3. Die Operation SELECT i1 from T1, die den Wert i1 gleich 100 zurückgibt, beginnt zum Zeitpunkt t8
  4. Transaktion #11 endet mit der COMMIT-Anweisung, d. h. die von Transaktion #11 vorgenommenen Änderungen werden in der Datenbank committet

So können wir mit Hilfe von Transaktionsdiagrammen im Detail beschreiben, was in der Datenbank vor sich geht, und erfahren, wie Transaktionen funktionieren.

Jetzt, wo wir Transaktionsdiagramme haben, schauen wir uns an, was das ACID-Akronym wirklich bedeutet.

Atomarität

Atomarität bedeutet, dass entweder alle Operationen, die eine Transaktion ausmachen, ausgeführt werden oder keine von ihnen: “alles oder nichts”. Das scheint ein Kinderspiel zu sein, aber dann kommen Details ans Licht.

Erstens hat das DBMS (nicht nur Firebird, sondern fast alle) zwei Arten von Atomarität: Atomarität auf der Ebene einer Anweisung und Atomarität auf der Ebene einer Gruppe von Anweisungen innerhalb einer Transaktion.

Atomarität auf Anweisungsebene bedeutet, dass die Anweisung UPDATET1 SETX=1 WHEREY=2 immer entweder erfolgreich ausgeführt wird oder nicht.

Atomarität auf der Ebene einer Gruppe von Anweisungen funktioniert anders (wir verwenden hier Pseudocode, um zu markieren, wann die Transaktion gestartet und committet wird):

Code
Start transaction 11
INSERT ..100
INSERT ..200
INSERT ..300
Commit 11

So wird das ungefähr im Diagramm aussehen:

Das bedeutet, dass alle drei INSERT-Anweisungen erfolgreich ausgeführt werden und die von ihnen vorgenommenen Änderungen in dem Moment committet werden, in dem Transaktion #11 committet wird.

Die Frage, die ich in Workshops oft stelle, wenn es um Transaktionen geht - wird die COMMIT-Anweisung für Transaktion 11 erfolgreich ausgeführt, wenn INSERT INTO..300 eine Ausnahme auslöst:

Ein beträchtlicher Teil des Publikums antwortet immer, dass die COMMIT-Anweisung nicht erfolgreich ausgeführt wird! (Interessanterweise führt dies in einigen anderen DBMS dazu, dass die Transaktion zurückgerollt wird!)

Das ist jedoch nicht wahr - führen Sie einfach isql aus und experimentieren Sie mit einer beliebigen Datenbank (isql hat eine einfache und unkomplizierte Implementierung von Operationen, ohne “Raten” für den Benutzer).

Die Sache ist die, dass die Atomarität auf der Ebene von Anweisungsgruppen, die durch das Committen von Transaktionen gewährleistet wird, eine Frage der Geschäftslogik ist. Der Entwickler einer Anwendung muss entscheiden, ob eine Transaktion im Falle einer Ausnahme in der dritten INSERT-Anweisung committet werden soll oder nicht. Wenn die Geschäftslogik es ermöglicht, das Ergebnis zu committen, kann die COMMIT-Anweisung problemlos ausgeführt werden.

Somit ist die Atomaritätsanforderung in ACID die Anforderung, dass das DBMS die Ergebnisse einer Gruppe von Anweisungen, die innerhalb einer Transaktion ausgeführt werden, committen oder zurückrollen kann. Die Entscheidung, sie zu committen oder zurückzurollen, hängt von der Geschäftslogik ab, die Sie implementieren müssen.

Und lassen Sie uns noch einmal betonen - obwohl die Atomarität einer Transaktion für eine Gruppe von Anweisungen die Möglichkeit bedeutet, die gesamte Gruppe unabhängig von den Ergebnissen zu committen oder zurückzurollen (und die Wahl hängt von der Geschäftslogik ab), wird die Atomarität einer einzelnen Anweisung durch die Implementierung des DBMS garantiert, d. h. es ist unmöglich, eine einzelne Anweisung (z. B. UPDATE) “unvollständig” (nicht atomar) auszuführen.

Konsistenz

Konsistenz bedeutet, dass die Daten in der Datenbank keine Widersprüche aufweisen. Natürlich sehen wir hier ein ganzes Feld für Spekulationen, denn “was bedeutet ‘keine Widersprüche aufweisen’ überhaupt”?

Zwei Konsistenzebenen werden normalerweise unterschieden:

  1. Datenbankebene, wo Konsistenz die Übereinstimmung der Daten mit den Datenbankbeschränkungen wie Primary, Unique und Foreign Keys, Checks bedeutet. Diese Konsistenzebene wird dadurch gewährleistet, dass Datenbankbeschränkungen es nicht ermöglichen, Daten einzufügen, die nicht den Beschränkungen entsprechen: z. B. CHECK(x>0) erlaubt nicht, dass eine negative Zahl in das entsprechende Feld eingefügt wird.
  2. Ebene der Geschäftslogik, wo die Konsistenz vom Entwickler der Anwendung mit Hilfe von Werkzeugen, die das DBMS bietet, wie Transaktionen, sichergestellt wird.

Wie helfen Transaktionen, die Konsistenz auf der Ebene der Geschäftslogik sicherzustellen? Ganz einfach - wenn wir das Beispiel der Geldüberweisung nehmen, muss der Entwickler sicherstellen, dass alle Änderungen im Falle einer Ausnahme zurückgerollt werden, und die Verwendung einer Transaktion hilft ihm dabei.

Code
Starttransaction
Den Geldbetrag auf Konto 1 verringern…. Erfolg
Auf Konto 2 erhöhen… Fehler
Rollback ---- im Falle einer Ausnahme!

Mit anderen Worten, der Entwickler muss den Code so schreiben, dass die Daten im Falle einer Ausnahme zurückgerollt werden und so die Konsistenz der Daten aus Sicht der Geschäftslogik erhalten bleibt.

Auf diese Weise bedeutet die Konsistenzanforderung im ACID-Akronym, dass es für das DBMS notwendig ist, die Möglichkeit zu haben, die Konsistenz der Daten mit Hilfe des Transaktionsmechanismus aufrechtzuerhalten.

Isolation

Die Anforderung der Transaktionsisolation ergibt sich aus der Notwendigkeit, das Ergebnis einer Reihe von Operationen unabhängig von der Reihenfolge, in der sie ausgeführt werden, zu garantieren.

Einfach ausgedrückt, muss jede Transaktion mit ein und demselben Ergebnis ausgeführt werden, unabhängig davon, ob gleichzeitig andere Transaktionen aktiv sind.

Der Mechanismus der Transaktionen soll die Konsistenz auf der Ebene der Geschäftslogik sicherstellen, aber er soll auch Transaktionen vor vorübergehenden unbestätigten Daten schützen, die während der Ausführung gleichzeitiger Transaktionen auftreten können.

In der Praxis sieht das so aus:

Wir sehen Transaktion #11, die zum Zeitpunkt t2 gestartet wird, innerhalb derer zum Zeitpunkt t3-t5 ein Insert in die Tabelle vorgenommen wird. Transaktion #11 wird nicht direkt nach dem Insert committet, sondern bleibt bis zum Zeitpunkt t8 aktiv.

Gleichzeitig wird Transaktion #12 gestartet, die die SELECT-Anweisung für die Tabellendatensätze ausführt, in die innerhalb von Transaktion #11 eingefügt wurde. Die erste SELECT-Anweisung wird zum Zeitpunkt t6 ausgeführt, wenn die Insert-Operation bereits beendet ist, aber diese Anweisung gibt ein leeres Ergebnis zurück, weil Transaktion #12 keine unbestätigten Daten aus anderen Transaktionen sehen kann.

Transaktion #11 wird zum Zeitpunkt t8 committet und die SELECT-Anweisung innerhalb von Transaktion #12 wird zum Zeitpunkt t9 ausgeführt. Sie gibt das Ergebnis gleich 100 zurück, weil die innerhalb von Transaktion #11 erstellten Daten jetzt committet sind (und weil die Isolationsstufe von Transaktion #12 Read Committed ist, aber darüber werden wir später sprechen).

Dieses Beispiel reicht völlig aus, um die Anforderung der Isolation zu veranschaulichen - anders als Atomarität und Konsistenz wird Isolation als strenge Regeln umgesetzt, die als Isolationsstufen bezeichnet werden, und jede Transaktion muss einen Parameter haben, der die Isolationsstufe festlegt, mit der sie arbeitet.

Dauerhaftigkeit

Das Konzept der Dauerhaftigkeit erlaubt es dem Entwickler, sich vollständig darauf zu verlassen, dass Daten, die innerhalb einer festgeschriebenen Transaktion erstellt wurden, sofort in der Datenbank erscheinen und nicht aus ihr verschwinden (natürlich ohne explizite Anweisungen, die sie löschen oder ändern), egal was als Nächstes passiert.

Wie Sie sehen können, ist die Anforderung der Dauerhaftigkeit nur gesunder Menschenverstand - kaum jemand würde zustimmen, ein System zu verwenden, dessen Daten plötzlich verschwinden könnten.

ACID: Zusammenfassung

ACID bedeutet die Anforderungen, wie Transaktionen funktionieren müssen:

  • Atomarität
    • Anweisungen sind immer atomar
    • Gruppen von Anweisungen können mit Hilfe von Transaktionen atomar gemacht werden
  • Konsistenz
    • Zwei Konsistenzebenen: Datenbankbeschränkungen und Geschäftslogik
  • Isolation
    • Gewährleistet durch den Mechanismus von Transaktionen mit Hilfe der für sie festgelegten Isolationsstufen
  • Dauerhaftigkeit
    • Alle festgeschriebenen Daten werden dauerhaft

Wie Sie sehen, ist alles ziemlich logisch. In der Praxis stellt die größte Schwierigkeit die Isolationsstufen dar, also lassen Sie uns im Detail sehen, wie sie funktionieren.

Die Isolationsstufe einer Transaktion definiert, welche festgeschriebenen Daten diese Transaktion sehen kann.

Es gibt Isolationsstufen, die üblicherweise als Standard bezeichnet werden. Sie sind im ANSI-SQL-Standard (verschiedene Revisionen) beschrieben. Soweit ich weiß, gibt es kein einziges DBMS, in dem sie genau so implementiert sind, wie sie im Standard beschrieben sind, aber niemand macht sich darüber Sorgen, da die tatsächlichen Mechanismen von Transaktionen in spezifischen DBMS alle notwendigen Optionen für die Implementierung der Geschäftslogik haben.

Sie finden die klassische Definition der Isolationsstufen in „A Critique of ANSI SQL Isolation Levels

Für diejenigen, die diesen Artikel gelesen haben, hier die Tabelle, die die klassischen Isolationsstufen mit ähnlichen in Firebird vergleicht. Natürlich ist die Entsprechung nicht direkt, da Isolationsstufen in Firebird, wie auch in anderen DBMS, nicht zu 100 % den ANSI-SQL-Definitionen entsprechen, aber ihnen sehr ähnlich sind.

ANSI-Isolationsstufen Isolationsstufe in Firebird
Read Uncommitted n/a
Read Committed Read Committed
Repeatable Read Snapshot
Serializable Snapshot-Tabellenstabilität

Wie jedes andere DBMS hat Firebird seine Besonderheiten bei der Implementierung der Isolation. Jetzt konzentrieren wir uns darauf, wie Isolationsstufen in Firebird funktionieren, anstatt darauf, wie gut sie dem Standard entsprechen.

Snapshot-Isolationsstufe

Die Snapshot-Isolationsstufe war die erste im ursprünglichen Code von InterBase und bleibt die Standardeinstellung für die Firebird-Kern-API und Dienstprogramme (zum Beispiel isql.exe). Dies könnte der Grund sein, warum sie am einfachsten zu verstehen ist.

Snapshot isoliert die Transaktion von allen Änderungen, die ab dem Moment ihres Starts vorgenommen werden.

Werfen wir einen Blick auf das Transaktionsdiagramm unten: Es zeigt Transaktion #10, die mit der Snapshot-Isolationsstufe gestartet wurde. Innerhalb dieser Transaktion werden mehrere SELECT-Anweisungen für Tabelle T1 ausgeführt, die in diesem Beispiel keine Datensätze enthält.

Die gleichzeitige Transaktion #15, die nach dem Start von Transaktion #10 beginnt, fügt Daten in Tabelle T1 ein, und diese Transaktion endet mit der COMMIT-Anweisung zum Zeitpunkt t9, d.h. die Daten werden zu diesem Zeitpunkt in der Datenbank festgeschrieben und sind für Anweisungen aus anderen Transaktionen verfügbar.

Die Anweisung in Transaktion #10, die zum Zeitpunkt t10 ausgeführt wird (d.h. nachdem Transaktion #15 festgeschrieben wurde), sieht die eingefügten Daten jedoch nicht, da die Snapshot-Isolationsstufe ihr erlaubt, nur festgeschriebene Daten zu sehen, die VOR DEM START von Transaktion #10 eingefügt oder geändert wurden.

Somit ermöglicht die Snapshot-Isolationsstufe die Arbeit mit der Datenbank, als ob sie zum Zeitpunkt des Transaktionsstarts eingefroren wäre. Sie ist normalerweise notwendig, um komplexe Berichte auf der Grundlage sich schnell ändernder Daten zu erstellen: Snapshot wird verwendet, um die Situation zu vermeiden, in der der erste Teil des Berichts auf bestimmten Daten basiert und der letzte Teil auf anderen Daten.

Diese großartige Funktion hat jedoch ihren Preis - wenn wir später untersuchen, wie Isolation in Firebird implementiert ist, werden Sie sehen, dass das Starten sehr langer Transaktionen mit der Snapshot-Isolationsstufe zu übermäßigen Datensatzversionen und geringerer Leistung führt.

Read Committed-Isolationsstufe

Eine Transaktion mit der Read Committed-Isolationsstufe kann die festgeschriebenen Daten anderer Transaktionen sehen, die festgeschrieben werden, während sie aktiv ist (anders als bei der Snapshot-Stufe, bei der nur Daten gesehen werden können, die vor dem Start der Transaktion festgeschrieben wurden).

Lassen Sie uns zeigen, wie die Read Committed-Isolationsstufe funktioniert, indem wir das folgende Diagramm verwenden:

Es zeigt ein Beispiel, das praktisch identisch mit dem vorherigen ist: zwei gleichzeitige Transaktionen, von denen eine regelmäßig Daten aus Tabelle T1 liest, während die zweite Daten einfügt und festschreibt.

Anders als bei der Snapshot-Isolationsstufe sieht Transaktion #10 in diesem Beispiel die Daten, die von Transaktion #15 eingefügt und festgeschrieben wurden.

Dieses Beispiel gibt uns eine Vorstellung von der Wirkung der Read Committed-Isolationsstufe: Anweisungen innerhalb einer Transaktion mit dieser Isolationsstufe können Daten sehen, die vor dem Moment der Ausführung der entsprechenden Anweisung festgeschrieben wurden.

Das nächste Diagramm zeigt ein Beispiel, in dem zwei gleichzeitige Transaktionen #11 und #18 Daten ändern.

Beachten Sie, dass Transaktion #11 vor dem Start von Transaktion #14 beginnt, die Daten liest, während Transaktion #18 nach ihr beginnt, aber dies beeinflusst das Ergebnis nicht: Wenn die Daten festgeschrieben sind, können sie von der gleichzeitigen Transaktion mit der Read Committed-Isolationsstufe gesehen werden.

Diese Möglichkeit macht die Read Committed-Isolationsstufe zu einer natürlichen Wahl für SQL-Anweisungen, die regelmäßig ausgeführt werden, um den neuesten Datenbankzustand anzuzeigen (zum Beispiel, um die neuesten Bestellungen anzuzeigen).

Der Teil, der der Garbage Collection gewidmet ist, wird zeigen, dass Read Committed-Transaktionen mit dem Read-Only-Modifikator in Firebird bis Version 4 die beste Wahl für „unendliche“ Lesetransaktionen sind, da sie als vorab festgeschrieben gestartet werden.

Snapshot-Tabellenstabilitäts-Isolationsstufe

Es ist möglich, die Geschichte über die Snapshot-Tabellenstabilitäts-Isolationsstufe, die ein Gegenstück zur standardmäßigen Serializable-Isolationsstufe ist, entweder sehr kurz oder ziemlich lang und detailliert zu erzählen.

Die kurze Version der Geschichte ist die folgende: Diese Stufe ist völlig ähnlich zur Snapshot-Stufe, mit dem zusätzlichen Sperren der Tabelle (die Tabelle muss explizit in den Transaktionsparametern angegeben werden) für Schreiben und Lesen. Das bedeutet, dass es möglich ist, eine Transaktion zu starten, die die angegebene Tabelle vollständig belegt, und alle anderen Transaktionen erhalten Zugriffsfehler.

Mit anderen Worten, eine Transaktion mit der Snapshot-Tabellenstabilitäts-Isolationsstufe wird tatsächlich alle Abfragen auf die angegebene Tabelle in eine Warteschlange stellen. Tatsächlich werden nur Lesevorgänge in regulären Transaktionen außer der Reihe ausgeführt (wie üblich), während alle anderen Modi eine Warteschlange bilden (es hängt natürlich von der Interaktion ab).

Wenn dies ohne Vorsicht implementiert wird, kann es Sperren und die Unmöglichkeit verursachen, mit der Datenbank zu arbeiten, weshalb Firebird-Datenbankanwendungsentwickler Angst haben könnten, diese Isolationsstufe zu verwenden.

Wenn jedoch korrekt implementiert, ermöglicht die Serializable-Isolationsstufe, leicht Warteschlangen zu bilden und sequenzielle Änderungen an Datenbankdatensätzen vorzunehmen, was sehr nützlich sein kann, um Zähler, sequenzielle Dokumentnummern und andere ähnliche Objekte zu implementieren.

Um korrekt zu beschreiben, wie man mit Hilfe einer Transaktion mit der Snapshot-Tabellenstabilitäts-Isolationsstufe eine Warteschlange bildet, müssen wir uns einen weiteren Transaktionsparameter ansehen: wait/nowait - und dann zum Warteschlangenbeispiel zurückkehren.

Zuvor haben wir eine solche Art der Interaktion zwischen Transaktionen untersucht, bei der Daten innerhalb einer Transaktion geändert und innerhalb einer anderen Transaktion gelesen werden.

Es kommt jedoch in der Praxis oft vor, dass verschiedene Transaktionen versuchen, dieselben Daten zu ändern, und da nur ein Ergebnis in der Datenbank gespeichert wird, erhält die gleichzeitige Transaktion eine Konfliktmeldung - tatsächlich eine Ausnahme, die die Ausführung der spezifischen Anweisung unterbricht (und abbricht), die versucht, die bereits geänderten Daten zu ändern.

Die wait-Option definiert, wie eine Transaktion auf den Aktualisierungskonflikt reagieren soll. Es gibt drei Möglichkeiten, diese Option zu konfigurieren:

  1. Wait (keine Parameter) = warten, bis die gleichzeitige Transaktion endet
  2. Wait Timeout N sec = warten, bis die gleichzeitige Transaktion endet, aber nicht länger als N Sekunden
  3. Nowait - nicht warten, bis die gleichzeitige Transaktion endet

Beachten Sie, dass die wait-Option hier im Pseudocode angegeben ist, während die Namen in der API und in den spezifischen Komponenten unterschiedlich sein können, obwohl die Bedeutung dieselbe bleibt.

Lassen Sie uns im Detail sehen, was bei Aktualisierungskonflikten mit verschiedenen Varianten der wait-Option passiert.

Wait

Stellen wir uns also zwei gleichzeitig aktive Transaktionen (#11 und #14) vor, innerhalb derer die UPDATE-Anweisung ausgeführt wird, die denselben Datensatz in derselben Tabelle T1 ändern muss.

Transaktion #14 läuft mit der wait-Option (wenn Sie isql verwenden, um die Beispiele zu reproduzieren, ist wait standardmäßig eingestellt).

Die UPDATE-Anweisung in Transaktion #11 beginnt zum Zeitpunkt t3 und endet zum Zeitpunkt t5, aber die Transaktion ist noch nicht festgeschrieben - d.h. die COMMIT-Anweisung ist bis zum Zeitpunkt t6 nicht vorhanden.

Das Diagramm unten zeigt diese Situation:

Die UPDATE-Anweisung wird auch in Transaktion #14 ausgeführt und versucht, denselben Datensatz in derselben Tabelle zu aktualisieren, beginnt aber später - ungefähr zum Zeitpunkt t4.

Da es einen Aktualisierungskonflikt mit dem Update aus Transaktion #11 gibt und wait in Transaktion #14 angegeben ist, wartet die UPDATE-Anweisung, bis die konfliktierende Transaktion #11 endet.

Wenn Transaktion #11 lange genug dauert, scheint die UPDATE-Anweisung in Transaktion #14 aus Sicht des Benutzers, der die Ausführung dieser Anweisung beobachtet, eingefroren zu sein.

Wenn Sie diese Situation mit Hilfe von zwei isql.exe reproduzieren, zeigt das nächste Bild den Moment, in dem die zweite Transaktion (genauer gesagt die Transaktion, in der die gleichzeitige UPDATE-Anweisung später beginnt - in unserem Beispiel Transaktion #14) wartet, bis die erste Transaktion endet (in unserem Beispiel Transaktion #11).

Nachdem die COMMIT-Anweisung in Transaktion #11 ausgeführt wurde, wird Transaktion #14, die darauf wartet, sofort benachrichtigt, und das konfliktierende Update endet mit einer Ausnahme.

Unten sehen Sie ein Beispiel für eine solche Fehlermeldung (die Nummer der gleichzeitigen Transaktion stimmt nicht mit unserem Beispiel überein, da die Transaktionsnummern in jeder Datenbank von vorne beginnen und dann nur inkrementiert werden, während sie nur nach Backup/Restore zurückgesetzt werden):

Code
SQL> update T1 set i1 = 2 where i1=1;
Statement failed, SQLSTATE = 40001
deadlock
-update conflicts with concurrent update
-concurrent transaction number is 19
SQL>

Achten Sie auf das Wort „deadlock“ in der Fehlermeldung - jetzt gibt es tatsächlich keinen Deadlock gemäß seiner klassischen Definition. Stattdessen gibt es einen Aktualisierungskonflikt, aber Firebird-Entwickler ändern die Fehlermeldung nicht, da sie seit mehr als 35 Jahren verwendet wird. Wir werden uns später mit dem echten „klassischen“ Deadlock befassen.

Wir haben also die Situation untersucht, in der die Transaktion mit der gleichzeitigen UPDATE-Anweisung mit der COMMIT-Anweisung endet. Betrachten wir nun eine ähnliche Situation, aber wenn sie zurückgerollt wird - Sie können sie im Diagramm unten sehen:

Die Situation ist völlig ähnlich zur vorherigen - zwei UPDATE-Anweisungen versuchen, denselben Datensatz zu aktualisieren, aber die gleichzeitige Transaktion #20 wird diesmal zurückgerollt, und die Änderungen innerhalb von Transaktion #15 werden als Ergebnis ohne Fehler in der Datenbank gespeichert.

Somit ermöglicht die Wait-Option, die Geschäftslogik von Updates so zu organisieren, dass konkurrierende Updates unendlich lange in einer Warteschlange warten und bis zum letzten Moment hoffen, dass die mit ihnen kollidierende Transaktion mit der ROLLBACK-Anweisung endet.

Ergibt diese Taktik immer Sinn? Natürlich hängt es von der Implementierung der Geschäftslogik ab, aber Firebird bietet auch andere Optionen zur Lösung von Update-Konflikten mit Hilfe der Wait-Option an.

Wait mit Timeout

Zunächst einmal kann es eine gute Idee sein, die Wartezeit zu begrenzen - anstatt bei einem Konflikt unendlich zu warten, können Sie die Wartezeit begrenzen, indem Sie einen Timeout für die Wait-Option angeben.

In isql.exe wird ein solcher Parameter mit Hilfe der folgenden Anweisung angegeben:

Code
SET TRANSACTION WAIT LOCK TIMEOUT N;

Wobei N die Zeit (in Sekunden) ist, die die parallele Transaktion auf die Lösung des Konflikts warten wird.

Weitere Details zu Transaktionssteuerungsanweisungen finden Sie im Firebird Language Reference. Beachten Sie, dass es in bestimmten Treibern oder Zugriffskomponenten unterschiedliche Möglichkeiten geben kann, den Timeout anzugeben (normalerweise mit Hilfe des API-Parameters).

Ein Beispiel in isql sehen Sie im folgenden Bild:

Lassen Sie uns mit Hilfe von Transaktionsdiagrammen untersuchen, wie Transaktionen interagieren, wenn Sie den Timeout für die Wait-Option angeben.

Die Situation ist also dieselbe - zwei parallele Transaktionen #11 und #14, innerhalb derer die UPDATE-Anweisung ausgeführt wird, die versucht, denselben Datensatz in Tabelle T1 zu aktualisieren.

In diesem Fall wartet die Anweisung innerhalb der Transaktion #14 jedoch entweder, bis Transaktion #11 endet, oder bis der angegebene Timeout (3 Sekunden) abläuft - je nachdem, was zuerst eintritt.

In diesem Beispiel läuft der Timeout zuerst ab, die Anweisung endet mit einer Ausnahme:

Code
SQL> update T1 set i1=6 where i1=1;
Statement failed, SQLSTATE = 40001
lock time-out on wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 40

Beachten Sie, dass “deadlock” wieder in der Fehlermeldung steht, aber es ist immer noch kein “echter” Deadlock.

Entsprechend ist die Situation ähnlich wie bei der Wait-Option, aber sie ist durch den Timeout begrenzt - wenn der angegebene Timeout früher abläuft als die parallele Transaktion endet.

Die Angabe der Wait-Option mit Timeout kann eine gute Lösung zur Implementierung von Geschäftslogik sein, wenn Sie sicher wissen, dass alle schreibenden Transaktionen recht kurz sind (zum Beispiel nicht länger als 1-2 Sekunden).

Nowait

Es ist sehr einfach, Nowait aus formaler Sicht zu erklären - es ist Wait mit einem Timeout von null. Wenn Sie nowait in Transaktionen angeben, lösen konkurrierende Updates sofort eine Ausnahme aus.

In diesem Fall haben wir wieder parallele Transaktionen #11 und #14 (nowait), in denen die parallelen UPDATE-Anweisungen ausgeführt werden. Die Anweisung innerhalb der Transaktion mit der Nowait-Option wartet nicht, wenn sie ein paralleles Update sieht, sondern löst sofort im Moment ihres Updates die folgende Ausnahme aus (nur die Transaktionsnummer ist anders):

Code
SQL> update T1 set i1=5 where i1=1;
Statement failed, SQLSTATE = 40001
lock conflict on no wait transaction
-deadlock
-update conflicts with concurrent update
-concurrent transaction number is 50
SQL>

So sieht es in einem Beispiel mit zwei isql-Tools aus:

Beachten Sie, dass die Nowait-Transaktion sich nicht darum kümmert, wann und wie die Transaktion mit der parallelen UPDATE-Anweisung endet - ob es die COMMIT- oder ROLLBACK-Anweisung sein wird, die Ausnahme wird trotzdem ausgelöst.

Aus Sicht der Geschäftslogik kann die Nowait-Transaktion praktisch sein, wenn Sie sicher wissen, dass das parallele Update zweifellos zum Abbruch der Aktionen der aktuellen Anweisung führen muss.

Viele Firebird-Treiber verwenden die Nowait-Option als Standardwert, und solange viele Entwickler nicht wissen, dass es möglich ist, ein weniger strenges Niveau zur Lösung von Update-Konflikten festzulegen (zum Beispiel wait lock timeout 1), leiden ihre Anwendungen (und manchmal auch Benutzer) unter unnötigen Fehlern aufgrund von Konflikten.

Da das Schlüsselwort “deadlock” in jeder Ausnahme im Zusammenhang mit Update-Konflikten vorhanden ist, sind sich viele Anwendungsentwickler sicher, dass dies ein echter Deadlock ist (einige denken sogar, dass ein gewisser Dead eine Rolle bei diesem Fehler gespielt hat).

Wenn wir uns gleichzeitig die Konfigurationsdatei firebird.conf ansehen, sehen wir dort den Parameter DeadlockTimeout (standardmäßig 10 Sekunden), und wenn wir uns den Ausgabekopf des Dienstprogramms fb_lock_print ansehen, sehen wir auch den Parameter “Deadlock scans”.

Die Sache ist die, dass ein “echter Deadlock” in Firebird möglich ist und das Schlüsselwort “deadlock”, das in allen Ausnahmen im Zusammenhang mit Update-Konflikten erscheint, keinen direkten Zusammenhang damit hat. Glücklicherweise kommt der echte Deadlock eher selten vor.

Lassen Sie uns sehen, was dieser “echte Deadlock” ist. Dazu werfen wir einen Blick auf das folgende Diagramm der Transaktionsinteraktion:

Wir haben zwei parallele Transaktionen mit der Wait-Option, in denen die UPDATE-Anweisung ausgeführt wird. Anders als bei einem einfachen Update-Konflikt sehen wir hier einen voneinander abhängigen Update-Konflikt:

  • Transaktion #11 aktualisiert den Datensatz mit dem Schlüssel = 20, und Transaktion #12 aktualisiert den Datensatz mit dem Schlüssel = 10;
  • Danach aktualisiert Transaktion #11 den Datensatz mit dem Schlüssel = 10, und Transaktion #12 aktualisiert den Datensatz mit dem Schlüssel = 20;

Als Ergebnis haben wir eine Situation, in der jede Transaktion auf das Ende der anderen warten muss und beide möglicherweise unendlich warten, weil für beide die Wait-Option angegeben ist. Natürlich kann der Server das nicht zulassen, also wird eine der Transaktionen nach Ablauf des im Parameter DeadlockTimeout angegebenen Timeouts (standardmäßig 10 Sekunden) zum Rollback gezwungen.

Wir können diese Situation mit Hilfe von zwei isql reproduzieren:

Nachdem die zweite Transaktion gestartet wurde, tritt eine Situation eines echten Deadlocks auf. Um dies sicher herauszufinden, startet der Server eine Prozedur namens Deadlock scan - sie wird in Intervallen gestartet, die DeadlockTimeout entsprechen, das standardmäßig 10 Sekunden beträgt.

Beachten Sie, dass der Client (in diesem Fall isql) eine normale Update-Konfliktmeldung erhält, aber sie wird in 10 Sekunden ausgelöst, selbst wenn die Transaktion mit der Wait-Option gestartet wurde.

Nachdem der Server die gegenseitige Sperre zweier Transaktionen erkannt hat, erhöht er auch den internen Deadlock-Zähler (Sie können ihn in der fb_lock_print-Ausgabe sehen).

Praktische Verwendung von Snapshot Table Stability

Jetzt, wo wir wissen, wie Transaktionen mit konkurrierenden UPDATE-Anweisungen arbeiten, können wir zur Isolationsstufe Snapshot Table Stability zurückkehren und eine praktische Verwendung dafür finden.

Wenn diese Isolationsstufe angegeben ist, wird die Tabelle für Schreibvorgänge und sogar für Lesevorgänge gesperrt.

Beachten Sie, dass, wenn die Tabelle in den Transaktionsparametern nicht explizit angegeben ist, alle Tabellen, auf die Anweisungen innerhalb dieser Transaktion zugreifen, gesperrt werden, und dies geschieht beim ersten Zugriff auf eine Tabelle. Offensichtlich führt diese Isolationsstufe, wenn sie ohne Vorsicht verwendet wird, leicht zu einer großen Anzahl von Update-Konflikten.

Die Klausel Reserving TableNN ermöglicht es Ihnen, eine bestimmte Tabelle (oder mehrere Tabellen) anzugeben, die zu Beginn der Transaktion gesperrt werden soll (es ist auch möglich, den Reservierungsmodus anzugeben).

Diese großartige Funktion zusammen mit der Wait-Option ermöglicht es Ihnen, eine sehr effektive sequenzielle Warteschlange für die Änderung einer bestimmten Tabelle zu implementieren.

In der Praxis sieht es so aus - diejenigen Clients, die eine Warteschlange zu einer bestimmten Tabelle erstellen müssen, starten die SNAPSHOT TABLE STABILITY-Transaktion, geben diese Tabelle an, versuchen dann, innerhalb dieser Transaktion eine Operation durchzuführen, und beenden sie sofort.

Zum Beispiel möchten wir einen sequenziell inkrementierenden Zähler in einer Tabelle mit dem einzigen Datensatz des Typs CREATE TABLE Table1(i1 integer not null) erstellen, aber wir können aus irgendeinem Grund keinen Generator verwenden.

Der Pseudocode sieht ungefähr so aus:

Code
set transaction snapshot table stability reserving TABLE1 for protected write
UPDATE Table1 Set i1 = i1+1;
SELECT i1 from Table1;
COMMIT;

Wenn wir diesen Code nicht mit der Isolationsstufe Snapshot Table Stability (Table1), sondern mit einer niedrigeren Isolationsstufe ausführen, ist es möglich, dass eine parallele UPDATE-Anweisung zwischen dem Start der Transaktion und vor ihrer UPDATE-Anweisung eingreift. Als Ergebnis erhalten wir entweder sofort eine Update-Ausnahme (nowait) oder die Anweisung friert bis zum Ende der parallelen ein (wait) oder bis zum Timeout (wait interval) - mit anderen Worten, der Konflikt wird irgendwie auf Anweisungsebene gelöst.

Mit der Isolationsstufe Snapshot Table Stability sind wir dagegen geschützt, weil die Tabelle zu Beginn der Transaktion reserviert ist - sie ist entweder vollständig unser oder vollständig nicht unser. Wenn wir die Wait-Option zur Lösung von Konflikten angeben, bilden die parallelen Verbindungen automatisch eine Warteschlange, ohne Fehler zu behandeln.

Natürlich kann dieser Ansatz nur auf kurze Transaktionen angewendet werden (so wie in unserem Beispiel).

In der Praxis wird die Isolationsstufe Snapshot Table Stability verwendet, um Warteschlangen zu bilden und komplexe Logik im exklusiven Modus neu zu berechnen (in relativ kleinen Tabellen oder wenn es keine anderen Benutzer gibt).

Im Inneren des Motors verwendet Firebird die Isolationsstufe Snapshot Table Stability, um Indizes zu erstellen - d.h. wenn Sie die Anweisung ALTER INDEX indexname ACTIVE; ausführen, belegt Firebird die Tabelle, für die der Index erstellt wird, vollständig.

Was kommt als Nächstes?

Dieser Artikel gibt nur eine Einführung in die Konzepte von Firebird-Transaktionen. Um vollständig zu verstehen, wie Transaktionen in Firebird funktionieren, ist es notwendig, die mehrgenerationale Architektur (Datensatzversionen und Garbage-Collection-Konzepte) zu betrachten, Transaktionsmarker (Oldest Interesting, Oldest Active, Oldest Snapshot, next) und andere Dinge zu berücksichtigen.

Der Artikel basiert auf den Materialien des Seminars/Workshops “All About Transactions”, das erstmals 2013 während der Firebird Tour-Seminare vorgestellt wurde, sowie auf der Grundlage des Trainings von IBSurgeon " Firebird Transaction in details".

Kontakte

[email protected] Bitte zögern Sie nicht, uns bei Fragen oder Anregungen zu kontaktieren: [email protected]