
InterSystems IRIS DBMS unterstützt kuriose Datenspeicherstrukturen - globale. Tatsächlich handelt es sich hierbei um mehrstufige Schlüssel mit verschiedenen zusätzlichen Vorteilen in Form von Transaktionen, schnellen Funktionen zum Durchlaufen von Datenbäumen, Sperren und ihrer eigenen ObjectScript-Sprache.
Mehr über Globals in der Artikelserie „Globals - Swords-Masons for Data Storage“:
Die Bäume. Teil 1Die Bäume. Teil 2Spärliche Arrays. Teil 3Es wurde für mich interessant, wie Transaktionen global implementiert werden, welche Funktionen es gibt. Immerhin ist dies eine völlig andere Struktur zum Speichern von Daten als die üblichen Tabellen. Viel niedrigeres Niveau.
Wie Sie aus der relationalen Datenbanktheorie wissen, muss eine gute Transaktionsimplementierung die
ACID- Anforderungen erfüllen:
A - Atom (Atomizität). Alle an der Transaktion vorgenommenen oder gar keine Änderungen werden aufgezeichnet.
C - Konsistenz. Nach Abschluss der Transaktion muss der logische Status der Datenbank intern konsistent sein. In vielerlei Hinsicht gilt diese Anforderung für den Programmierer, bei SQL-Datenbanken jedoch auch für Fremdschlüssel.
I - Isolieren (Isolieren). Parallele Transaktionen sollten sich nicht gegenseitig beeinflussen.
D - Langlebig. Nach erfolgreichem Abschluss der Transaktion sollten Probleme auf den unteren Ebenen (z. B. Stromausfall) keine Auswirkungen auf die durch die Transaktion geänderten Daten haben.
Globale sind nicht relationale Datenstrukturen. Sie wurden für ultraschnelle Arbeiten mit sehr begrenzter Hardware entwickelt. Lassen Sie uns die Implementierung von Transaktionen in Globals anhand des
offiziellen IRIS-Docker-Images verstehen.
Zur Unterstützung von Transaktionen in IRIS werden die folgenden Befehle verwendet:
TSTART ,
TCOMMIT ,
TROLLBACK .
1. Atomizität
Der einfachste Weg, um die Atomizität zu überprüfen. Überprüfung über die Datenbankkonsole.
Kill ^a TSTART Set ^a(1) = 1 Set ^a(2) = 2 Set ^a(3) = 3 TCOMMIT
Dann schließen wir:
Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)
Wir bekommen:
1 2 3
Alles ist in Ordnung. Atomizität beobachtet: Alle Änderungen werden aufgezeichnet.
Wir erschweren die Aufgabe, führen einen Fehler ein und sehen, wie die Transaktion teilweise oder gar nicht gespeichert wird.
Lassen Sie uns die Atomizität noch einmal überprüfen:
Kill ^A TSTART Set ^a(1) = 1 Set ^a(2) = 2 Set ^a(3) = 3
Dann den Container gewaltsam anhalten, starten und sehen.
docker kill my-iris
Dieser Befehl entspricht fast dem gewaltsamen Ausschalten der Stromversorgung, da er ein Signal sendet, um den SIGKILL-Prozess sofort zu stoppen.
Vielleicht wurde die Transaktion teilweise gespeichert?
WRITE ^a(1), ^a(2), ^a(3) ^ <UNDEFINED> ^a(1)
- Nein, nicht erhalten.
Testen Sie 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) ^ <UNDEFINED> ^a(1)
Auch nichts blieb erhalten.
2. Konsistenz
Da in Datenbanken auf Globals Schlüssel auch auf Globals erstellt werden (ich erinnere mich, dass ein Global eine Struktur auf niedrigerer Ebene zum Speichern von Daten als eine relationale Tabelle ist), müssen Sie die Schlüsseländerung in dieselbe Transaktion wie die globale Änderung aufnehmen, um die Konsistenzanforderung zu erfüllen.
Zum Beispiel haben wir eine globale Person, in der wir Persönlichkeiten speichern und die TIN als Schlüssel verwenden.
^person(1234567, 'firstname') = 'Sergey' ^person(1234567, 'lastname') = 'Kamenev' ^person(1234567, 'phone') = '+74995555555 ...
Um eine schnelle Suche nach Nachname und Vorname zu haben, haben wir den Schlüsselindex ^ erstellt.
^index('Kamenev', 'Sergey', 1234567) = 1
Damit die Basis vereinbart werden kann, müssen wir Persönlichkeiten wie diese hinzufügen:
TSTART ^person(1234567, 'firstname') = 'Sergey' ^person(1234567, 'lastname') = 'Kamenev' ^person(1234567, 'phone') = '+74995555555 ^index('Kamenev', 'Sergey', 1234567) = 1 TCOMMIT
Dementsprechend sollten wir beim Löschen auch die Transaktion verwenden:
TSTART Kill ^person(1234567) ZKill ^index('Kamenev', 'Sergey', 1234567) TCOMMIT
Mit anderen Worten, die Erfüllung der Konsistenzanforderung liegt vollständig beim Programmierer. Aber wenn es um Globale geht, ist dies aufgrund ihrer niedrigen Natur normal.
3. Isolierung
Hier beginnt die Wildnis. Viele Benutzer arbeiten gleichzeitig an derselben Datenbank und ändern dieselben Daten.
Die Situation ist vergleichbar mit der Situation, in der viele Benutzer gleichzeitig mit demselben Code mit dem Code arbeiten und versuchen, Änderungen an vielen Dateien gleichzeitig zu übernehmen.
Die Datenbank sollte dies in Echtzeit beheben. Angesichts der Tatsache, dass es in seriösen Unternehmen sogar eine spezielle Person gibt, die für die Versionskontrolle verantwortlich ist (für das Zusammenführen von Zweigen, das Lösen von Konflikten usw.), und die Datenbank sollte dies alles in Echtzeit tun, die Komplexität der Aufgabe und das korrekte Design der Datenbank und der Code, der es dient.
Die Datenbank kann die Bedeutung der von Benutzern ausgeführten Aktionen nicht verstehen, um Konflikte zu vermeiden, wenn sie mit denselben Daten arbeiten. Es kann nur eine Transaktion im Gegensatz zu einer anderen abbrechen oder nacheinander ausführen.
Ein weiteres Problem besteht darin, dass während der Ausführung der Transaktion (vor dem Festschreiben) der Status der Datenbank möglicherweise inkonsistent ist. Daher ist es wünschenswert, dass andere Transaktionen keinen Zugriff auf den inkonsistenten Status der Datenbank haben, der in relationalen Datenbanken auf vielfältige Weise erreicht wird: Erstellen von Snapshots, Multiversionszeilen und usw.
Bei der parallelen Ausführung von Transaktionen ist es uns wichtig, dass sie sich nicht gegenseitig stören. Dies ist die Eigenschaft der Isolation.
SQL definiert 4 Isolationsstufen:
- LESEN SIE UNVERPFLICHTET
- LESEN SIE VERPFLICHTET
- WIEDERHOLBAR LESEN
- SERIALISIERBAR
Betrachten wir jedes Level einzeln. Die Kosten für die Implementierung jeder Ebene steigen fast exponentiell.
READ UNCOMMITTED ist die niedrigste Isolationsstufe, aber die schnellste. Transaktionen können von einander vorgenommene Änderungen lesen.
READ COMMITTED ist die nächste Stufe der Isolation, die einen Kompromiss darstellt. Transaktionen können keine Änderungen lesen, die vor einem Commit voneinander vorgenommen wurden, können jedoch alle Änderungen lesen, die nach einem Commit vorgenommen wurden.
Wenn wir eine lange Transaktion T1 haben, bei der es Commits in den Transaktionen T2, T3 ... Tn gab, die mit denselben Daten wie T1 arbeiteten, erhalten wir jedes Mal unterschiedliche Ergebnisse, wenn wir Daten in T1 anfordern. Dieses Phänomen wird als nicht wiederholbares Lesen bezeichnet.
WIEDERHOLBARES LESEN - In dieser Isolationsstufe tritt das Phänomen des nicht wiederholbaren Lesens nicht auf, da für jede Anforderung zum Lesen von Daten ein Snapshot der Ergebnisdaten erstellt wird und bei Wiederverwendung in derselben Transaktion die Daten aus dem Snapshot verwendet werden. Bei dieser Isolationsstufe können jedoch Phantomdaten gelesen werden. Dies bezieht sich auf das Lesen neuer Zeilen, die durch gleichzeitig festgeschriebene Transaktionen hinzugefügt wurden.
SERIALIZABLE ist die höchste Isolationsstufe. Es zeichnet sich dadurch aus, dass die in der Transaktion in irgendeiner Weise verwendeten Daten (Lesen oder Ändern) erst nach Abschluss der ersten Transaktion anderen Transaktionen zur Verfügung stehen.
Lassen Sie uns zunächst herausfinden, ob Operationen in einer Transaktion vom Hauptthread isoliert sind. Lassen Sie uns 2 Terminalfenster öffnen.
Es gibt keine Isolation. Ein Thread sieht, was der zweite, der die Transaktion geöffnet hat, tut.
Mal sehen, ob Transaktionen mit unterschiedlichen Flüssen sehen, was in ihnen passiert.
Wir öffnen 2 Terminalfenster und öffnen 2 Transaktionen parallel.
Gleichzeitige Transaktionen sehen die Daten des jeweils anderen. So haben wir die einfachste, aber auch die schnellste Isolationsstufe READ UNCOMMITED.
Im Prinzip ist dies für Globals zu erwarten, bei denen Geschwindigkeit immer an erster Stelle stand.
Aber was ist, wenn wir ein höheres Maß an Isolation bei globalen Operationen benötigen?
Hier müssen Sie darüber nachdenken, warum Isolationsstufen benötigt werden und wie sie funktionieren.
Die höchste Isolationsstufe von SERIALIZE bedeutet, dass das Ergebnis gleichzeitig ausgeführter Transaktionen ihrer sequentiellen Ausführung entspricht, wodurch das Fehlen von Kollisionen garantiert wird.
Wir können dies mithilfe kompetenter Sperren in ObjectScript tun, die viele verschiedene Anwendungsmöglichkeiten haben: Mit dem Befehl
LOCK können Sie regelmäßige, inkrementelle Mehrfachsperren durchführen.
Niedrigere Isolationsstufen sind Kompromisse, um die Geschwindigkeit der Datenbank zu erhöhen.
Mal sehen, wie wir mit Sperren unterschiedliche Isolationsstufen erreichen können.
Mit diesem Operator können Sie nicht nur die exklusiven Sperren verwenden, die zum Ändern von Daten erforderlich sind, sondern auch die sogenannten freigegebenen Sperren, die mehrere Threads gleichzeitig benötigen, wenn sie Daten lesen müssen, die während des Lesens nicht von anderen Prozessen geändert werden sollen.
Weitere Informationen zur Zwei-Phasen-Verriegelungsmethode in Russisch und Englisch:
→
Zweiphasensperre→
ZweiphasenverriegelungDie Schwierigkeit besteht darin, dass während der Transaktion der Status der Datenbank inkonsistent sein kann, diese inkonsistenten Daten jedoch für andere Prozesse sichtbar sind. Wie vermeide ich das?
Mithilfe von Sperren erstellen wir solche Sichtbarkeitsfenster, in denen der Status der Datenbank vereinbart wird. Und alle Anrufe an solche Sichtfenster des vereinbarten Zustands werden durch Schlösser kontrolliert.
Gemeinsame Sperren derselben Daten können wiederverwendet werden - mehrere Prozesse können sie verwenden. Diese Sperren verhindern, dass andere Prozesse Daten ändern, d. H. Sie werden verwendet, um die Fenster des koordinierten Status der Datenbank zu bilden.
Exklusive Sperren werden zum Ändern von Daten verwendet - nur ein Prozess kann eine solche Sperre durchführen. Exklusives Blockieren kann dauern:
- Jeder Prozess, wenn Daten frei sind
- Nur der Prozess, der eine gemeinsame Sperre für diese Daten hat, und der erste haben eine exklusive Sperre angefordert.

Je enger das Sichtbarkeitsfenster ist, desto länger dauert es, bis andere Prozesse warten, aber desto konsistenter kann der Status der darin enthaltenen Datenbank sein.
READ_COMMITED - Das Wesentliche dieser Ebene ist, dass nur Daten aus anderen gesperrten Streams
angezeigt werden. Wenn die Daten in einer anderen Transaktion noch nicht festgeschrieben sind, sehen wir ihre alte Version.
Dies ermöglicht es uns, die Arbeit zu parallelisieren, anstatt darauf zu warten, dass die Sperre freigegeben wird.
Ohne spezielle Tricks können wir die alte Version der Daten in IRIS nicht sehen, daher haben wir es mit Sperren zu tun.
Dementsprechend müssen wir gemeinsam genutzte Sperren verwenden, um das Lesen von Daten nur in Momenten der Konsistenz zu ermöglichen.
Angenommen, wir haben eine Benutzerbasis ^ Person, die Geld untereinander überweist.
Der Zeitpunkt der Übertragung von Person 123 auf 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 Zeitpunkt der Anforderung des Geldbetrags von Person 123 vor der Abbuchung muss von einer exklusiven Sperre (standardmäßig) begleitet sein:
LOCK +^person(123) Write ^person(123)
Und wenn Sie den Kontostatus in Ihrem Konto anzeigen müssen, können Sie die gemeinsame Sperre verwenden oder sie überhaupt nicht verwenden:
LOCK +^person(123)
Wenn wir jedoch davon ausgehen, dass Datenbankoperationen fast sofort ausgeführt werden (ich erinnere mich, dass Globals eine viel niedrigere Struktur als eine relationale Tabelle haben), sinkt der Bedarf für diese Ebene.
REPEATABLE READ - In dieser Isolationsstufe wird davon
ausgegangen, dass mehrere Lesevorgänge von Daten möglich sind, die durch gleichzeitige Transaktionen geändert werden können.
Dementsprechend müssen wir das Lesen der Daten, die wir ändern, gemeinsam sperren und die Daten, die wir ändern, exklusiv sperren.
Glücklicherweise ermöglicht der LOCK-Operator einem Operator, alle erforderlichen Sperren, die sehr viele sein können, detailliert aufzulisten.
LOCK +^person(123, amount)
andere Operationen (zu diesem Zeitpunkt versuchen parallele Threads, ^ person (123, Betrag) zu ändern, können dies jedoch nicht)
LOCK +^person(123, amount) ^person(123, amount) LOCK -^person(123, amount) ^person(123, amount) LOCK -^person(123, amount)
Wenn Sie durch Kommas getrennte Sperren auflisten, werden diese nacheinander ausgeführt. Wenn Sie dies tun:
LOCK +(^person(123),^person(242))
dann werden sie alle auf einmal atomar genommen.
SERIALISIEREN - Wir müssen die Sperren setzen, damit letztendlich alle Transaktionen mit gemeinsamen Daten nacheinander ausgeführt werden. Für diesen Ansatz müssen die meisten Sperren exklusiv sein und für die Leistung in die kleinsten Bereiche der Welt gebracht werden.
Wenn wir über Abschreibungen in der globalen Person sprechen, ist nur die Isolationsstufe SERIALIZE für sie akzeptabel, da das Geld streng nacheinander ausgegeben werden muss, andernfalls ist es möglich, denselben Betrag mehrmals auszugeben.
4. Haltbarkeit
Ich führte Tests durch, bei denen der Behälter hart durchgeschnitten wurde
docker kill my-iris
Die Basis vertrug sie gut. Es wurden keine Probleme festgestellt.
Fazit
Für Globals bietet InterSystems IRIS Transaktionsunterstützung. Sie sind wirklich atomar, zuverlässig. Um die Datenbankkonsistenz auf globalen Daten zu gewährleisten, sind die Bemühungen des Programmierers und die Verwendung von Transaktionen erforderlich, da keine komplexen integrierten Konstruktionen wie Fremdschlüssel vorhanden sind.
Die Isolationsstufe von Globals ohne Verwendung von Sperren ist READ UNCOMMITED und kann bei Verwendung von Sperren bis zur SERIALIZE-Ebene sichergestellt werden.
Die Richtigkeit und Geschwindigkeit von Transaktionen auf globalen Daten hängt stark von den Fähigkeiten des Programmierers ab: Je häufiger gemeinsam genutzte Sperren beim Lesen verwendet werden, desto höher ist die Isolationsstufe und je mehr exklusive Sperren verwendet werden, desto höher ist die Geschwindigkeit.