Transaktionen in den Globals von InterSystems IRIS

Transaktionen in den Globals von InterSystems IRISDie 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“:

BĂ€ume. Teil 1
BĂ€ume. Teil 2
Sparsame Arrays. Teil 3

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. ACID:

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 dem offiziellen Docker-Image von IRIS.

Zur UnterstĂŒtzung von Transaktionen in IRIS werden folgende Befehle verwendet: TSTART, TCOMMIT, TROLLBACK.

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
TCOMMIT

Danach machen wir eine Ausgabe:

Write ^a(1), “ ”, ^a(2), “ ”, ^a(3)

Wir erhalten:

1 2 3

Alles 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) = 3

Danach werden wir den Container gewaltsam anhalten, neu starten und schauen.

docker kill my-iris

Dieser 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) = 1

Damit 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
TCOMMIT

Dementsprechend mĂŒssen wir auch beim Löschen eine Transaktion verwenden:

TSTART
Kill ^person(1234567)
ZKill ^index('Kamenev', 'Sergey', 1234567)
TCOMMIT

Mit 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)=2

Es 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 LOCK.

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:

→ Zweiphasige Sperre
→ Two-phase locking

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:

  1. Jedem Prozess, sofern die Daten frei sind
  2. Nur dem Prozess, der eine shared-Sperre fĂŒr die Daten hat und der als erster eine exklusive Sperre angefordert hat.

Transaktionen in den Globals von InterSystems IRIS

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-iris

Die 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

60GB SSD 8Gb DDR4