Die Datenbank InterSystems IRIS unterstützt ausgeklügelte Strukturen zur Speicherung von Daten – Globals. Im Wesentlichen handelt es sich um mehrstufige Schlüssel mit verschiedenen zusätzlichen Funktionen wie Transaktionen, schnellen Funktionen zur Durchquerung von Datenbäumen, Sperren und einer eigenen Sprache, ObjectScript.
Mehr über Globals in der Artikelreihe „Globals – die Schätze zur Datenspeicherung“:
Mich interessiert, wie Transaktionen in Globals implementiert sind und welche Besonderheiten es gibt. Schließlich handelt es sich um eine ganz andere Struktur zur Datenspeicherung als die alltäglichen Tabellen. Sie ist viel niedrigerebene.
Wie bekannt, muss eine gute Implementierung von Transaktionen gemäß der Theorie relationaler Datenbanken den Anforderungen gerecht werden. :
A – Atomarität. Alle in der Transaktion vorgenommenen Änderungen werden entweder festgeschrieben oder es werden gar keine vorgenommen.
C – Konsistenz. Nach Abschluss der Transaktion muss der logische Zustand der Datenbank intern konsistent sein. Vieles in dieser Anforderung betrifft den Programmierer, betrifft aber bei SQL-Datenbanken auch die Fremdschlüssel.
I – Isolierung. Parallel ausgeführte Transaktionen dürfen sich nicht gegenseitig beeinflussen.
D – Dauerhaftigkeit. Nach erfolgreichem Abschluss der Transaktion dürfen Probleme auf unteren Ebenen (z. B. Stromausfall) die von der Transaktion geänderten Daten nicht beeinträchtigen.
Globals sind nicht-relationale Datenstrukturen. Sie wurden für extrem schnelle Arbeiten auf sehr begrenzter Hardware entwickelt. Lassen Sie uns die Implementierung von Transaktionen in Globals anhand von .
Zur Unterstützung von Transaktionen in IRIS werden folgende Befehle verwendet: , , .
1. Atomarität
Die Atomarität lässt sich am einfachsten überprüfen. Wir prüfen aus der Datenbankkonsole.
Kill ^a
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TCOMMITDanach machen wir eine Ausgabe:
Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)Wir erhalten:
1 2 3Alles in Ordnung. Die Atomarität ist gewährleistet: Alle Änderungen wurden gespeichert.
Lassen Sie uns die Aufgabe komplizieren, indem wir einen Fehler einführen und sehen, wie die Transaktion gespeichert wird, teilweise oder gar nicht.
Überprüfen wir noch einmal die Atomarität:
Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3Danach werden wir den Container gewaltsam anhalten, neu starten und schauen.
docker kill my-irisDieser Befehl ist praktisch gleichbedeutend mit einem gewaltsamen Ausschalten der Stromversorgung, da er das Signal SIGKILL zum sofortigen Stoppen des Prozesses sendet.
Könnte die Transaktion teilweise gespeichert worden sein?
WRITE ^a(1), ^a(2), ^a(3)
^
^a(1)— Nein, es wurde nichts gespeichert.
Wir testen den Rollback-Befehl:
Kill ^A
TSTART
Set ^a(1) = 1
Set ^a(2) = 2
Set ^a(3) = 3
TROLLBACK
WRITE ^a(1), ^a(2), ^a(3)
^
^a(1)Es wurde ebenfalls nichts gespeichert.
2. Konsistenz
Da in Datenbanken die Schlüssel auch auf globalen Ebenen erstellt werden (ich erinnere daran, dass ein Globaler eine niedrigere Struktur zur Datenspeicherung ist als eine relationale Tabelle), muss die Änderung des Schlüssels in derselben Transaktion enthalten sein wie die Änderung des Globals, um die Konsistenz zu gewährleisten.
Angenommen, wir haben einen Globalen ^person, in dem wir Persönlichkeiten speichern und als Schlüssel die Steueridentifikationsnummer (INN) verwenden.
^person(1234567, 'firstname') = 'Sergey'
^person(1234567, 'lastname') = 'Kamenev'
^person(1234567, 'phone') = '+74995555555
...Um eine schnelle Suche nach Nachnamen und Vornamen zu ermöglichen, haben wir den Schlüssel ^index erstellt.
^index('Kamenev', 'Sergey', 1234567) = 1Damit die Datenbank konsistent bleibt, müssen wir die Persönlichkeit so hinzufügen:
TSTART
^person(1234567, 'firstname') = 'Sergey'
^person(1234567, 'lastname') = 'Kamenev'
^person(1234567, 'phone') = '+74995555555
^index('Kamenev', 'Sergey', 1234567) = 1
TCOMMITDementsprechend müssen wir auch beim Löschen eine Transaktion verwenden:
TSTART
Kill ^person(1234567)
ZKill ^index('Kamenev', 'Sergey', 1234567)
TCOMMITMit anderen Worten liegt die Erfüllung der Konsistenzanforderungen vollständig in der Verantwortung des Programmierers. Aber wenn es um Globale geht, ist das aufgrund ihrer niedrigeren Natur normal.
3. Isolation
Hier fangen die Schwierigkeiten an. Viele Benutzer arbeiten gleichzeitig an derselben Datenbank und ändern dieselben Daten.
Die Situation ist vergleichbar damit, wenn viele Benutzer gleichzeitig an demselben Code-Repository arbeiten und versuchen, gleichzeitig Änderungen an vielen Dateien zu committen.
Die Datenbank muss all dies in Echtzeit bewältigen. Angesichts dessen, dass es in großen Unternehmen sogar spezielle Personen gibt, die für die Versionskontrolle (für das Zusammenführen von Branches, das Lösen von Konflikten usw.) verantwortlich sind, und die DB all dies in Echtzeit erledigen muss, wird die Komplexität der Aufgabe und die Richtigkeit des Designs der Datenbank sowie des dazugehörigen Codes offensichtlich.
Die Datenbank kann die Bedeutung der von den Benutzern ausgeführten Aktionen nicht verstehen, um Konflikte zu vermeiden, wenn sie an denselben Daten arbeiten. Sie kann nur eine transaktionale Handlung stornieren, die einer anderen widerspricht, oder sie nacheinander ausführen.
Ein weiteres Problem ist, dass während der Ausführung einer Transaktion (vor dem Commit) der Zustand der Datenbank inkonsistent sein kann, weshalb es wünschenswert ist, dass andere Transaktionen keinen Zugriff auf den inkonsistenten Zustand der Datenbank haben. Das wird in relationalen Datenbanken auf vielfältige Weise erreicht: durch das Erstellen von Snapshots, Mehrversionsverwaltung von Zeilen usw.
Bei der parallelen Ausführung von Transaktionen ist es wichtig, dass sie sich nicht gegenseitig stören. Das ist das Eigenschaft der Isolation.
SQL definiert vier Isolationsebenen:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
Betrachten wir jede Ebene einzeln. Die Kosten für die Umsetzung jeder Ebene steigen nahezu exponentiell.
READ UNCOMMITTED — das ist die niedrigste Isolationsebene, aber gleichzeitig die schnellste. Transaktionen können Änderungen lesen, die von anderen vorgenommen wurden.
READ COMMITTED — das ist die nächste Isolationsebene, die einen Kompromiss darstellt. Transaktionen können Änderungen, die von anderen vorgenommen wurden, bis zum Commit nicht lesen, aber sie können alle Änderungen lesen, die nach dem Commit vorgenommen wurden.
Wenn wir eine lange Transaktion T1 haben, während der in den Transaktionen T2, T3 … Tn Commits erfolgt sind, die mit denselben Daten arbeiten, die auch T1 verwendet, dann erhalten wir bei der Abfrage von Daten in T1 jedes Mal unterschiedliche Ergebnisse. Dieses Phänomen wird nicht wiederholbares Lesen genannt.
REPEATABLE READ — in dieser Isolationsebene haben wir das Phänomen des nicht wiederholbaren Lesens nicht, da für jede Datenleseanfrage ein Snapshot der Ergebnisdaten erstellt wird, und bei einer Wiederverwendung in derselben Transaktion werden die Daten aus dem Snapshot verwendet. In dieser Isolationsebene ist jedoch das Lesen von Phantomdaten möglich. Dabei handelt es sich um das Lesen neuer Zeilen, die von parallelen, festgeschriebenen Transaktionen hinzugefügt wurden.
SERIALIZABLE — die höchste Isolationsebene. Sie ist dadurch charakterisiert, dass Daten, die in einer Transaktion auf irgendeine Weise verwendet werden (lesen oder ändern), für andere Transaktionen erst nach Abschluss der ersten Transaktion verfügbar sind.
Zunächst klären wir, ob die Isolierung der Vorgänge in der Transaktion vom Hauptstrom vorhanden ist. Wir öffnen 2 Terminalfenster.
Kill ^t
Write ^t(1)
2
TSTART
Set ^t(1)=2Es gibt keine Isolierung. Ein Thread sieht, was der andere, der die Transaktion eröffnet hat, tut.
Schauen wir uns an, ob die Transaktionen verschiedener Threads das sehen, was in ihnen passiert.
Wir öffnen 2 Terminalfenster und führen 2 Transaktionen parallel aus.
kill ^t
TSTART
Write ^t(1)
3
TSTART
Set ^t(1)=3
Parallele Transaktionen sehen die Daten einander. Damit haben wir die einfachste, aber auch schnellste Isolationsstufe READ UNCOMMITTED erreicht.
Im Grunde konnte man das bei Globals erwarten, für die Geschwindigkeit immer an erster Stelle stand.
Was tun, wenn wir eine höhere Isolationsstufe bei Vorgängen mit Globals benötigen?
Hier sollte man sich fragen, warum Isolationsstufen überhaupt benötigt werden und wie sie funktionieren.
Die höchste Isolationsstufe SERIALIZE bedeutet, dass das Ergebnis parallel ausgeführter Transaktionen dem ihrer sequentiellen Ausführung entspricht, was eine Kollision ausschließt.
Das können wir mit intelligenten Locks in ObjectScript erreichen, die viele Anwendungsmöglichkeiten bieten: man kann reguläre, inkrementelle, oder Mehrfachsperren mit dem Befehl .
Niedrigere Isolationsstufen sind Kompromisse, die darauf abzielen, die Datenbankgeschwindigkeit zu erhöhen.
Schauen wir uns an, wie wir verschiedene Isolationsstufen mit Sperren erreichen können.
Dieser Befehl erlaubt es, nicht nur exklusive Sperren zu setzen, die für Datenänderungen nötig sind, sondern auch sogenannte shared, die von mehreren Threads parallel genutzt werden können, wenn sie Daten lesen müssen, die von anderen Prozessen während des Lesens nicht geändert werden dürfen.
Mehr über das zweiphasige Sperrverfahren auf Russisch und Englisch:
→
→
Das Problem liegt darin, dass der Zustand der Datenbank während einer Transaktion inkonsistent sein kann, diese inkonsistenten Daten jedoch anderen Prozessen sichtbar sind. Wie kann man das vermeiden?
Wir werden mithilfe von Sperren Sichtfenster schaffen, in denen der Datenbankzustand konsistent sein wird. Alle Zugriffe auf solche Sichtfenster des konsistenten Zustandes werden durch Sperren kontrolliert.
Shared-Sperren für dieselben Daten sind mehrfach — sie können von mehreren Prozessen übernommen werden. Diese Sperren verhindern, dass andere Prozesse die Daten ändern, d. h. sie werden zur Erstellung von Fenstern mit konsistentem Zustand der Datenbank verwendet.
Exklusive Sperren werden für Datenänderungen verwendet — eine solche Sperre kann nur von einem Prozess übernommen werden. Eine exklusive Sperre kann übernommen werden von:
- Jedem Prozess, sofern die Daten frei sind
- Nur dem Prozess, der eine shared-Sperre für die Daten hat und der als erster eine exklusive Sperre angefordert hat.

Je enger das Sichtfenster, desto länger müssen andere Prozesse warten, aber desto konsistenter kann der Zustand der Datenbank darin sein.
READ_COMMITTED — das Wesentliche dieses Levels ist, dass wir nur bestätigte Daten aus anderen Streams sehen. Wenn die Daten in einer anderen Transaktion noch nicht bestätigt sind, sehen wir ihre alte Version.
Dies ermöglicht es uns, die Arbeit zu parallelisieren, anstatt auf die Freigabe der Sperre zu warten.
Ohne spezielle Tricks können wir die alte Version der Daten in IRIS nicht sehen, daher müssen wir mit Sperren auskommen.
Demnach müssen wir mithilfe von shared-Sperren das Lesen von Daten nur in Momenten der Konsistenz erlauben.
Angenommen, wir haben eine Benutzerdatenbank ^person, in der sie sich gegenseitig Geld überweisen.
Der Moment der Überweisung von Person 123 zu Person 242:
LOCK +^person(123), +^person(242)
Set ^person(123, amount) = ^person(123, amount) - amount
Set ^person(242, amount) = ^person(242, amount) + amount
LOCK -^person(123), -^person(242)Der Moment der Anforderung des Geldbetrags von Person 123 vor der Belastung sollte von einer exklusiven Sperre begleitet werden (standardmäßig):
LOCK +^person(123)
Write ^person(123)Und wenn der Kontostand im persönlichen Bereich angezeigt werden muss, kann entweder eine shared-Sperre verwendet werden oder sie kann ganz entfallen:
LOCK +^person(123)#”S”
Write ^person(123)Angenommen, die Datenbankoperationen werden praktisch im Handumdrehen ausgeführt (ich erinnere daran, dass Globals eine deutlich niedrigere Struktur sind als relationale Tabellen), dann ist die Notwendigkeit dieses Levels nicht gegeben.
REPEATABLE READ — in diesem Isolationslevel ist es erlaubt, dass es mehrere Lesevorgänge gibt, die von parallelen Transaktionen geändert werden können.
Demnach müssen wir eine shared-Sperre für die Daten, die wir ändern, und exklusive Sperren für die Daten, die wir ändern, setzen.
Der Operator LOCK ermöglicht es, in einem einzigen Operator alle notwendigen Sperren detailliert aufzulisten, von denen es sehr viele geben kann.
LOCK +^person(123, amount)#“S”
lesen ^person(123, amount)andere Operationen (währenddessen versuchen parallele Prozesse, ^person(123, amount) zu ändern, können dies aber nicht)
LOCK +^person(123, amount)
Änderung ^person(123, amount)
LOCK -^person(123, amount)
lesen ^person(123, amount)
LOCK -^person(123, amount)#“S”Beim Auflisten von Sperren durch Kommas werden sie nacheinander behandelt, wenn man es so macht:
LOCK +(^person(123),^person(242))werden sie alle atomar gleichzeitig erfasst.
SERIALIZE — wir müssen die Sperren so setzen, dass letztendlich alle Transaktionen, die gemeinsame Daten haben, nacheinander ausgeführt werden. Für diesen Ansatz müssen die meisten Sperren exklusiv sein und auf die kleinsten Bereiche des Globals gesetzt werden, um die Leistung zu optimieren.
Wenn es um Ausgaben aus dem Global ^person geht, ist nur das Isolationsniveau SERIALIZE akzeptabel, da Geld strikt nacheinander ausgegeben werden muss, andernfalls besteht die Möglichkeit, den gleichen Betrag mehrfach auszugeben.
4. Dauerhaftigkeit
Ich habe Tests mit einem harten Ausschalten des Containers durchgeführt durch
docker kill my-irisDie Datenbank hat dies gut überstanden. Es wurden keine Probleme festgestellt.
Fazit
Für Globals in InterSystems IRIS gibt es Unterstützung für Transaktionen. Sie sind tatsächlich atomar und zuverlässig. Um die Konsistenz der Datenbank in Globals zu gewährleisten, sind die Anstrengungen des Programmierers und die Verwendung von Transaktionen erforderlich, da es keine komplexen eingebauten Strukturen wie Fremdschlüssel gibt.
Das Isolationsniveau in Globals ohne Verwendung von Sperren ist READ UNCOMMITTED, und bei Verwendung von Sperren kann es bis hin zu SERIALIZE gewährleistet werden.
Die Korrektheit und Geschwindigkeit von Transaktionen in Globals hängt stark von den Fähigkeiten des Programmierers ab: Je breiter shared-Sperren beim Lesen verwendet werden, desto höher ist das Isolationsniveau, und je schmaler exklusive Sperren gesetzt werden, desto schneller ist die Leistung.
Quelle: habr.com
