Wir führen zunächst einige Grundlagen zur Synchronisation von Transaktionen ein, so die zu vermeidenden Mehrbenutzeranomalien sowie das Korrektheitskriterium der Serialisierbarkeit . Es folgt die Behandlung der wichtigsten Synchronisationstechniken (Sperrverfahren, Zeitstempelansätze und optimistische Verfahren), wobei meist zunächst kurz die Realisierung in zentralisierten DBS besprochen wird, bevor die Alternativen zur Realisierung in Verteilten DBS vorgestellt werden. Danach gehen wir auf die Realisierung einer Mehrversionensynchronisation ein, mit der das Ausmaß an Synchronisationskonflikten zwischen Transaktionen stark reduziert werden kann und die in derzeitigen DMS zunehmend an Bedeutung gewinnt. Abschließend werden dann die wichtigsten Alternativen zur Behandlung globaler Deadlocks vorgestellt.

Die meisten der Synchronisationsansätze wurden zunächst für geografisch verteilte relationale DBS vorgeschlagen, können aber oft auch für Parallele DBS vom Typ Shared Nothing sowie für NoSQL-Systeme eingesetzt werden. Tatsächlich streben NoSQL-Systeme zunehmend die Unterstützung der ACID-Eigenschaften an, z. B. auf Basis von Ansätzen der optimistischen und Mehrversionensynchronisation. Synchronisationsverfahren für Parallele DBS vom Typ Shared Disk werden in Kap. 14 behandelt.

1 Grundlagen der Synchronisation

Eine Schlüsseleigenschaft von DBS ist, dass viele Benutzer gleichzeitig lesend und ändernd auf die gemeinsamen Datenbestände zugreifen können, ohne dass die Konsistenz der Daten verletzt wird. Die Wahrung der DB-Konsistenz trotz paralleler Zugriffe ist Aufgabe der Synchronisationskomponente, wobei der Mehrbenutzerbetrieb gegenüber den Benutzern verborgen wird (Transparenz der konkurrierenden Verarbeitung, logischer Einbenutzerbetrieb). Werden alle Transaktionen seriell ausgeführt, dann ist der geforderte logische Einbenutzerbetrieb ohne jegliche Synchronisation erreicht und die DB-Konsistenz ist am Ende jeder Transaktion gewährleistet. Eine strikt serielle Ausführung der Transaktionen zur Umgehung der Synchronisationsproblematik verbietet sich jedoch v. a. aus Leistungsgründen. Denn im Einbenutzerbetrieb könnten die Prozessoren aufgrund langer Transaktionsunterbrechungen, z. B. wegen Kommunikations- oder E/A-Vorgängen, nicht vernünftig genutzt werden. Selbst für Hautspeicherdatenbanken erfordert die Nutzung mehrerer Cores bzw. Prozessoren (Shared Everything) die parallele Datenbankverarbeitung durch mehrere Threads bzw. Prozesse.

Beim unkontrollierten Zugriff auf Datenobjekte im Mehrbenutzerbetrieb können mehrere unerwünschte Phänomene auftreten. Die wichtigsten dieser Anomalien sind verloren gegangene Änderungen („lost update “), Lesen „schmutziger“ Änderungen („dirty read “), inkonsistente Analyse („non-repeatable read “) sowie sogenannte Phantome  [20]. Änderungen können verloren gehen, wenn zwei Transaktionen parallel dasselbe Objekt ändern, wobei die zuerst vorgenommene Änderung durch die zweite Transaktion fälschlicherweise überschrieben wird. Das Lesen schmutziger Änderungen, welche von noch nicht zu Ende gekommenen Transaktionen stammen, führt zu fehlerhaften Ergebnissen, wenn die ändernde Transaktion noch zurückgesetzt werden muss und somit ihre Änderungen ungültig werden. Die inkonsistente Analyse sowie das Phantomproblem betreffen Lesetransaktionen, die aufgrund parallel durchgeführter Änderungen während ihrer Ausführungszeit unterschiedliche DB-Zustände sehen. Diese Phänomene werden unter dem Begriff Mehrbenutzeranomalien zusammengefasst.

1.1 Serialisierbarkeitsbegriffe

Die genannten Anomalien werden vermieden durch Synchronisationsverfahren, welche das Korrektheitskriterium der Serialisierbarkeit erfüllen [15, 7]. Dieses Kriterium verlangt, dass das Ergebnis einer parallelen Transaktionsausführung äquivalent ist zu dem Ergebnis irgendeiner der seriellen Ausführungsreihenfolgen der beteiligten Transaktionen. Äquivalent bedeutet in diesem Zusammenhang, dass für jede der Transaktionen dieselbe Ausgabe wie in der seriellen Abarbeitungsreihenfolge abgeleitet wird und dass der gleiche DB-Endzustand erzeugt wird. In der Datenbanktheorie gibt es unterschiedliche Serialiserbarkeitsdefinitionen [40], wovon jedoch die sogenannte Konfliktserialisierbarkeit am bedeutsamsten ist und hier unterstellt wird. Dabei wird verlangt, dass in Konflikt stehende Operationen verschiedener Transaktionen (Zugriff auf dasselbe Datenobjekt, wobei mindestens einer der Zugriffe ändernd ist) in derselben Reihenfolge auszuführen sind, wie in der äquivalenten seriellen Ausführungsreihenfolge. Damit sieht eine Transaktion alle Änderungen der Transaktionen, die vor ihr in der äquivalenten seriellen Ausführungsreihenfolge stehen, jedoch keine der in dieser Reihenfolge nach ihr kommenden Transaktionen. Die zur parallelen Transaktionsbearbeitung äquivalente serielle Ausführungsreihenfolge wird auch als Serialisierungsreihenfolge bezeichnet.

1.2 Abhängigkeitsgraphen

Um zu überprüfen, ob ein bestimmter Schedule, d. h. eine Ablauffolge von Transaktionen mit ihren zugehörigen Operationen, serialisierbar ist, kann ein Serialisierbarkeits- oder Abhängigkeitsgraph geführt werden. In diesem Graphen treten die einzelnen Transaktionen als Knoten auf und die Abhängigkeiten zwischen Transaktionen als (gerichtete) Kanten. Eine Abhängigkeit zwischen zwei Transaktionen T i und T j liegt vor, wenn T i vor T j auf dasselbe Objekt zugegriffen hat und die Operationen der beiden Transaktionen nicht reihenfolgeunabhängig (kommutativ) sind. Werden (wie üblich) als Operationen Lese- und Schreibzugriffe betrachtet, dann liegt eine Abhängigkeit vor, wenn wenigstens eine der beiden Operationen eine Schreiboperation darstellt. Es kann gezeigt werden, dass ein Schedule genau dann serialisierbar ist, wenn der zugehörige Abhängigkeitsgraph azyklisch ist. Denn nur in diesem Fall reflektiert der Graph eine partielle Ordnung zwischen den Transaktionen, die zu einer vollständigen Ordnung, die zugleich den äquivalenten seriellen Schedule bestimmt, erweitert werden kann.

Beispiel 12.1

Abbildung 12.1a zeigt einen Schedule mit drei Transaktionen, wobei \(r(x)\) bzw. \(w(x)\) den Lese- bzw. Schreibzugriff der jeweiligen Transaktion auf Objekt x kennzeichnen. Der zugehörige Abhängigkeitsgraph ist in Abb. 12.1b gezeigt, wobei die Kanten zusätzlich mit dem die Abhängigkeit verursachenden Objekt gekennzeichnet sind. Da der Graph keinen Zyklus enthält, ist der gezeigte Schedule serialisierbar. Die Serialisierungsreihenfolge lautet \(T_{3}<T_{1}<T_{2}\).  \(\square\)

Abb. 12.1
figure 1

a Schedule und b Abhängigkeitsgraph (Beispiel)

Das Führen von Abhängigkeitsgraphen bietet jedoch keinen praktikablen Ansatz zur Implementierung eines Synchronisationsverfahrens, da hiermit meist erst nachträglich die Serialisierbarkeit von Schedules geprüft werden kann [33]. Weiterhin wäre der Verwaltungsaufwand prohibitiv hoch, zumal auch Abhängigkeiten zu bereits beendeten Transaktionen zu berücksichtigen sind. Zur Synchronisation wird daher auf andere Verfahren zurückgegriffen, für welche nachgewiesen werden konnte, dass sie Serialisierbarkeit gewährleisten. Die Mehrzahl der vorgeschlagenen Verfahren lässt sich einer der drei folgenden Klassen zuordnen: Sperrverfahren, optimistische Protokolle sowie Zeitmarkenverfahren.

1.3 Anforderungen für Verteilte DBS

Diese Verfahrensklassen kommen auch für Verteilte Datenbanksysteme in Betracht. Als neue Anforderung ergibt sich hier, eine systemweite Serialisierbarkeit aller lokalen und globalen Transaktionen zu erzielen (globale Serialisierbarkeit ). Hierzu reicht es nicht aus, nur die (lokale) Serialisierbarkeit in jedem Rechner zu gewährleisten, da in den einzelnen Rechnern unterschiedliche Serialisierungsreihenfolgen vorliegen können. Um eine hohe Leistungsfähigkeit zu erhalten, sollte diese Aufgabe wiederum mit möglichst geringem Kommunikationsaufwand erfüllt werden. Weiterhin sollte, wie bereits für zentralisierte DBS, die Synchronisation mit möglichst wenig Blockierungen und Rücksetzungen von Transaktionen erfolgen. Denn diese zur Behandlung von Synchronisationskonflikten verfügbaren Methoden haben beide einen negativen Einfluss auf Durchsatz und Antwortzeiten. Eine weitere Anforderung im verteilten Fall ist eine möglichst hohe Robustheit der Synchronisationsprotokolle gegenüber Fehlern (Rechnerausfall, Kommunikationsfehler). Erweiterungen werden daneben bei replizierten Datenbanken notwendig, um die wechselseitige Konsistenz von Replikaten sicherzustellen und Transaktionen mit aktuellen Objektkopien zu versorgen.

Da replizierte Datenbanken in Kap. 13 behandelt werden, konzentrieren wir uns hier zunächst auf die Synchronisation in partitionierten Datenbanken. Sperrverfahren und optimistische Verfahren können hierzu entsprechend erweitert werden und entweder unter zentralisierter Kontrolle (auf einem ausgezeichneten Knoten) oder verteilt durchgeführt werden. Für Zeitmarkenverfahren ist dagegen nur eine verteilte Realisierung sinnvoll. Wie schon in zentralisierten DBS können die einzelnen Verfahrensklassen auch in Verteilten DBS vielfältig miteinander kombiniert werden (z. B. Sperrverfahren mit optimistischen Protokollen).

2 Sperrverfahren in Verteilten DBS

Sperrverfahren sind dadurch gekennzeichnet, dass das DBMS vor dem Zugriff auf ein Objekt für die betreffende Transaktion eine Sperre erwirbt, deren Modus dem Zugriffswunsch entspricht. Zur Sicherstellung der Serialisierbarkeit ist dabei i. Allg. ein sogenanntes strikt zweiphasiges Sperrprotokoll erforderlich, bei dem sämtliche Sperren bis zum Transaktionsende gehalten werden (zweite Commit-Phase) [16]. Im einfachsten Fall wird nur zwischen Lese- und Schreibsperren unterschieden (RX-Sperrverfahren ). Dabei sind Lese- oder R-Sperren (read locks, shared locks) miteinander verträglich, während Schreib- oder X-Sperren (exclusive locks) weder mit sich selbst noch mit Lesesperren kompatibel sind. So können bei gesetzter R-Sperre auf ein Objekt weitere Leseanforderungen gewährt werden, jedoch keine X-Sperren; bei gesetzter X-Sperre sind alle weiteren Sperranforderungen abzulehnen. Ein Sperrkonflikt führt zur Blockierung der Transaktion, deren Sperranforderung den Konflikt verursacht hat; die Aktivierung der wartenden Transaktionen ist möglich, sobald die unverträglichen Sperren freigegeben sind.

Beispiel 12.2

Für den Schedule in Abb. 12.1a tritt mit einem RX-Protokoll ein Sperrkonflikt auf: Für die Lesesperre von T 2 auf Objekt y ergibt sich ein Konflikt aufgrund der zuvor gewährten X-Sperre für T 1. T 2 wird nach Beendigung und Freigabe der Sperren von T 1 fortgesetzt. Dagegen verursacht der Lesezugriff von T 1 auf x keinen Konflikt, da zu diesem Zeitpunkt T 3 bereits beendet ist. Als Serialisierungsreihenfolge ergibt sich \(T_{3}<T_{1}<T_{2}\).  \(\square\)

2.1 Zentrale Sperrprotokolle

Ein nahe liegender Ansatz zur Synchronisation in Verteilten DBS liegt darin, sämtliche Sperranforderungen und -freigaben auf einem dedizierten Rechner zu bearbeiten. Als Vorteil ergibt sich, dass die Synchronisation quasi wie in einem zentralisierten DBS abgewickelt werden kann, da im zentralen Knoten stets der aktuelle Synchronisationszustand bekannt ist. Insbesondere kann auch eine Deadlock-Erkennung wie im Ein-Rechner-Fall vorgenommen werden.

Allerdings sprechen erhebliche Nachteile gegen einen solchen Ansatz, sodass er für Verteilte DBS als ungeeignet anzusehen ist:

  • Jede Sperranforderung einer Transaktion, die nicht auf dem zentralen Knoten läuft, verursacht eine Nachricht, auf die die Transaktion synchron warten muss. Eine solche Nachrichtenhäufigkeit ist für Durchsatz und Antwortzeit gleichermaßen inakzeptabel.

  • Der zentrale Knoten stellt einen Engpass für Leistung und Verfügbarkeit (single point of failure) dar.

  • Es wird keine Knotenautonomie unterstützt.

Für Parallele DBS, insbesondere vom Typ Shared Disk, stellt sich die Bewertung anders dar, weil hier die Knotenautonomie keine Rolle spielt und die beiden erstgenannten Nachteile abgeschwächt werden können (s. Kap. 14).

2.2 Verteilte Sperrverfahren

Eine bessere Alternative stellen verteilte Sperrverfahren dar. Hierbei synchronisiert jeder Rechner alle Zugriffe auf die Daten der ihm zugeordneten Datenpartition. Diese lokale Sperrbehandlung erfordert keinerlei zusätzliche Kommunikation (bei fehlender Datenreplikation), da die verteilte Ausführung von Transaktionen und Operationen bereits auf die Datenverteilung abgestimmt wird. Kommunikation fällt also zum Starten der Teiltransaktionen an; für die Datenzugriffe während der Teiltransaktionen werden die benötigten Sperren lokal angefordert. Das Freigeben der Sperren erfolgt im Rahmen des Commit-Protokolls (Kap. 11). Das größte Problem verteilter Sperrverfahren ist die Behandlung globaler Deadlocks, die die Leistungsfähigkeit entscheidend beeinflussen kann. Auf die hierzu bestehenden Alternativen gehen wir in Abschn. 12.6 ein.

3 Zeitmarkenverfahren

Bei diesen Verfahren wird die Serialisierbarkeit durch Zeitstempel bzw. -marken an den Datenobjekten überprüft. Die Überprüfungen werden stets an den Speicherungsorten der Daten vorgenommen, sodass sich ein inhärent verteiltes Protokoll ergibt. Weiterhin entfallen eigene Kommunikationsvorgänge zur Synchronisation, ähnlich wie bei verteilten Sperrverfahren. Ein Hauptvorteil gegenüber Sperrverfahren liegt darin, dass keine Deadlocks vorkommen können.

3.1 Prinzip des Zeitmarkenverfahrens

Wir beschränken uns hier auf das einfachste Zeitmarkenverfahren (Basic Timestamp Ordering  [7]). Dabei bekommt jede Transaktion T bei ihrem BOT (Begin Of Transaction) eine global eindeutige Zeitmarke \(\textit{ts}(T)\) fest zugeordnet. Zur Sicherstellung der globalen Eindeutigkeit dieser Zeitmarken bietet sich die Verwendung zweiteiliger Zeitstempel bestehend aus lokaler Uhrzeit und Rechner-ID an [26]. Damit wird die globale Ordnung primär über die lokalen Uhrzeiten festgelegt. Die Rechner-ID wird nur für Transaktionen (verschiedener Rechner) mit übereinstimmender lokaler Uhrzeit benötigt, um eine vollständige Ordnung zu erreichen und die globale Eindeutigkeit sicherzustellen. Der Hauptvorteil eines solchen Ansatzes liegt darin, dass die Zeitstempel an jedem Knoten lokal vergeben werden können. Allerdings ist ohne Synchronisierung der lokalen Uhren [11] nicht gewährleistet, dass die Transaktionszeitstempel monoton wachsen. Es kann also sein, dass nach Beendigung einer Transaktion T 2 an einem anderen Knoten eine Transaktion mit kleinerem Zeitstempel T 1 gestartet wird.

Bei Zeitmarkenverfahren ist die Position einer Transaktion in der Serialisierungsreihenfolge bereits a priori durch ihre BOT-Zeitmarke festgelegt. Konfliktoperationen verschiedener Transaktionen müssen daher stets in der Reihenfolge der Transaktionszeitmarken erfolgen. Dies erfordert, dass eine Transaktion alle Änderungen von älteren Transaktionen (d. h. Transaktionen mit kleinerem Zeitstempel) sehen muss, jedoch keine Änderungen von „jüngeren“ Transaktionen sehen darf. Werden diese Bedingungen verletzt, wird die betroffene Transaktion zurückgesetzt und mit einem neuen Zeitstempel wiederholt.

3.2 Realisierung des Zeitmarkenverfahrens

Die Überprüfung dieser sehr restriktiven Forderungen geschieht mittels Zeitmarken an den Datenobjekten, wobei für jedes Objekt ein Schreib- und ein Lesezeitstempel geführt wird. Der Schreibzeitstempel (write time stamp) wts bzw. der Lesezeitstempel (read time stamp) rts entspricht dabei der Transaktionszeitmarke derjenigen Transaktion, die das Objekt zuletzt geändert bzw. gelesen hat. Ein Lesezugriff einer Transaktion T mit Zeitstempel \(\textit{ts}(T)\) auf ein Objekt x ist nicht zulässig, wenn bereits eine jüngere Transaktion als T das Objekt geändert hat, wenn also gilt: \(\textit{ts}(T)<\textit{wts}(x)\).

Analog darf für einen Schreibzugriff keine jüngere Transaktion das Objekt bereits geändert oder gelesen haben. Es darf also nicht gelten: \(\textit{ts}(T)<\max(\textit{rts}(x),\textit{wts}(x))\).

Liegt eine dieser Bedingungen vor, erfolgt die Rücksetzung der zugreifenden Transaktion T. Das informell beschriebene Verfahren kann in Pseudo-Code wie in Abb. 12.2 formuliert werden:

Abb. 12.2
figure 2

Verarbeitung eines Objektzugriffs \(p_{i}[x]\) von Transaktion T i auf Objekt x bei Zeitmarkensynchronisation

Beispiel 12.3

Für den Schedule in Abb. 12.1a erfolgen die Zugriffe auf Objekt y in der BOT-Reihenfolge der beiden Transaktionen T 1 und T 2, sodass kein Konflikt vorliegt. Dagegen greift T 1 erst nach der jüngeren Transaktion T 3 auf Objekt x zu, sodass T 1 zurückgesetzt wird. Als Serialisierungsreihenfolge ergibt sich \(T_{2}<T_{3}\).  \(\square\)

Die Rücksetzgefahr einer Transaktion steigt mit zunehmender Verweildauer im System, da dann entsprechend mehr jüngere Transaktionen auf die noch benötigten Datenobjekte zugreifen können. Damit besteht vor allem für lange Transaktionen eine hohe Rücksetzwahrscheinlichkeit. Weiterhin kann ein „Verhungern“ (Starvation) einer Transaktion nicht verhindert werden, da für eine bereits zurückgesetzte Transaktion wiederum eine Rücksetzung notwendig werden kann. Dies kann dazu führen, dass bestimmte Transaktionen u. U. nie zu Ende kommen.

Ein weiterer Nachteil ergibt sich daraus, dass „schmutzige“ Änderungen einer Transaktion durch Zusatzmaßnahmen gegenüber anderen Transaktionen zu verbergen sind. Für den Schedule in Abb. 12.1a ist so der Zugriff auf Objekt y durch T 2 aufgrund der Zeitmarken zwar zulässig. Jedoch stellt die von T 1 vorgenommene Änderung von y zu diesem Zeitpunkt eine schmutzige (vorläufige) Änderung dar, da T 1 noch kein Commit erreicht hat. In der Tat wird ja T 1 später aufgrund des Zugriffs auf Objekt x noch zurückgesetzt, sodass die von ihr vorgenommene Änderung von y zurückgenommen werden muss. Damit T 2 nicht die schmutzige Änderung sieht, muss ihr Zugriff auf y bis zum Ende von T 1 blockiert werden.

Das Beispiel verdeutlicht, dass nach einer Änderung wie bei Sperrverfahren alle weiteren Zugriffe (die nicht schon wegen des Zeitstempelvergleichs abgewiesen wurden) bis zum Transaktionsende des Änderers verzögert werden müssen. Damit kann ein ähnlich hohes Ausmaß an Blockierungen wie bei Sperrverfahren verursacht werden, zusätzlich zu den aufgrund der Zeitmarkenvergleiche eingeführten Rücksetzungen. Deadlocks (zyklische Wartebeziehungen zwischen Transaktionen) sind jedoch nicht möglich, da die Objektzugriffe stets in der Reihenfolge der BOT-Zeitstempel durchgeführt werden.

4 Optimistische Synchronisation

Optimistische Synchronisationsverfahren gehen von der Annahme aus, dass Konflikte zwischen Transaktionen seltene Ereignisse darstellen und somit das präventive Sperren der Objekte unnötigen Aufwand verursacht [25]. Daher greifen diese Verfahren zunächst nicht in den Ablauf einer Transaktion ein, sondern erlauben ein nahezu beliebig paralleles Arbeiten auf der Datenbank. Erst bei Transaktionsende wird überprüft, ob Konflikte mit anderen Transaktionen aufgetreten sind. Gemäß dieser Vorgehensweise unterteilt man die Ausführung einer Transaktion in drei Phasen:

  • In der Lesephase wird die eigentliche Transaktionsverarbeitung vorgenommen, d. h., es werden Objekte der Datenbank gelesen und modifiziert. Jede Transaktion führt dabei ihre Änderungen auf Kopien in einem ihr zugeordneten Transaktionspuffer durch, der für keine andere Transaktion zugänglich ist.

  • Bei EOT (End of Transaction) wird eine Validierungsphase gestartet, in der geprüft wird, ob die beendigungswillige Transaktion mit einer parallel zu ihr laufenden Transaktion in Konflikt geraten ist. Im Gegensatz zu Sperrverfahren, bei denen Blockierungen das primäre Mittel zur Behandlung von Synchronisationskonflikten sind, werden hier Konflikte stets durch Zurücksetzen einer oder mehrerer beteiligter Transaktionen aufgelöst. Es ist so zwar mit mehr Rücksetzungen als bei Sperrverfahren zu rechnen, dafür können aber bei optimistischen Verfahren keine Deadlocks entstehen. Dies ist vor allem für Verteilte DBS ein potenzieller Vorteil.

  • Die Schreibphase wird nur von Änderungstransaktionen ausgeführt, die die Validierungsphase erfolgreich beenden konnten. In dieser Phase wird zuerst die Wiederholbarkeit der Transaktion sichergestellt (Logging), bevor alle Änderungen durch Einbringen in die Datenbank für andere Transaktionen sichtbar gemacht werden.

4.1 Validierungsansätze

Um die Validierungen durchführen zu können, werden für jede Transaktion T i während ihrer Lesephase die Namen von ihr gelesener bzw. geänderter Objekte in einem Read-Set \(\textit{RS}(T_{i}\)) bzw. Write-Set \(\textit{WS}(T_{i})\) geführt. Wir nehmen an, dass vor jeder Änderung das entsprechende Objekt gelesen wird, sodass der Write-Set einer Transaktion stets eine Teilmenge des Read-Sets bildet. Nach [18] lassen sich optimistische Synchronisationsverfahren gemäß ihrer Validierungsstrategie grob in zwei Klassen unterteilen. Bei den rückwärts orientierten Verfahren (Backward Oriented Optimistic Concurrency Control , BOCC ) erfolgt die Validierung ausschließlich gegenüber bereits beendeten Transaktionen. Bei den vorwärts orientierten Verfahren (Forward Oriented Optimistic Concurrency Control , FOCC ) dagegen wird gegen noch laufende Transaktionen validiert. In beiden Fällen wird durch die Validierung sichergestellt, dass die validierende Transaktion alle Änderungen von zuvor erfolgreich validierten Transaktionen gesehen hat. Damit ist die Serialisierungsreihenfolge durch die Validierungsreihenfolge gegeben.

Beispiel 12.4

Abbildung 12.3 verdeutlicht die unterschiedliche Vorgehensweise bei vorwärtsorientierter und rückwärtsorientierter Validierung am Beispiel. Bei der Rückwärtsvalidierung wird die Transaktion T 1 gegen die Transaktionen T 2 und T 3 validiert, deren Persist-Phase sich mit der Execute-Phase von T 1 überlappen.

Bei dem zweiten Szenario wird T 4 in der Rückwärtsvalidierung gegen noch laufende Transaktionen getestet, deren Execute-Phase mit der aktuellen Validierungsaktion überlappt, also im Beispiel mit T 5 und T 6.  \(\square\)

Abb. 12.3
figure 3

Vorwärtsorientierte versus rückwärtsorientierte Validierung

Im ursprünglichen BOCC-Verfahren nach [25] wird bei der Validierung überprüft, ob die validierende Transaktion ein Objekt gelesen hat, das während ihrer Lesephase geändert wurde. Dazu wird in der Validierungsphase der Read-Set der validierenden Transaktion T j mit den Write-Sets aller Transaktionen T i verglichen, die während der Lesephase von T j validiert haben (Abb. 12.4a). Ergibt sich eine Überschneidung mit einem dieser Write-Sets, wird die validierende Transaktion zurückgesetzt, da sie möglicherweise auf veraltete Daten zugegriffen hat (die am Konflikt beteiligten Transaktionen können nicht mehr zurückgesetzt werden, da sie bereits beendet sind). Die Validierungen werden dabei in einem kritischen Abschnitt durchgeführt, der sicherstellt, dass zu einem Zeitpunkt höchstens eine Validierung vorgenommen wird.

Abb. 12.4
figure 4

Validierung einer Transaktion T j bei BOCC (a) und dem verbesserten BOCC+ (b)

Beispiel 12.5

Für den Schedule in Abb. 12.1a wird mit diesem BOCC-Protokoll zunächst T 3 erfolgreich validiert. Bei der nachfolgenden Validierung von T 1 wird festgestellt, dass das gelesene Objekt x sich im Write-Set der parallel ausgeführten und bereits validierten Transaktion T 3 befindet. Folglich wird T 1 zurückgesetzt. Die Validierung von T 2 schließlich ist erfolgreich. Es ergibt sich damit folgende Serialisierungsreihenfolge: \(T_{3}<T_{2}\).  \(\square\)

Ein generelles Problem optimistischer Verfahren ist, dass also Transaktionen bei der Validierung ständig scheitern und somit „verhungern“ können. Dies ist v. a. für lange Transaktionen zu befürchten, da sie einen großen Read-Set aufweisen und sich gegenüber vielen Transaktionen validieren müssen. Die skizzierte BOCC-Validierung hat zudem den Nachteil, dass Transaktionen oft unnötigerweise (wegen eines „unechten“ Konfliktes) zurückgesetzt werden, obwohl die aktuellen Objektversionen gesehen wurden. Dies ist dann der Fall, wenn auf das von einer parallelen Transaktion geänderte Objekt erst nach dem Einbringen in die Datenbank zugegriffen wurde. So wird in obigem Beispiel (Abb. 12.1a) T 1 unnötigerweise zurückgesetzt, weil die Änderung von T 3 gesehen wurde. Diese unnötigen Rücksetzungen betreffen v. a. lange Transaktionen, die bereits durch kurze Änderer zum Scheitern gezwungen werden können.

Eine Abhilfe des zuletzt genannten Problems wird jedoch einfach möglich, in dem man Änderungszähler oder Versionsnummern an den Objekten führt und eine Rücksetzung nur vornimmt, wenn tatsächlich veraltete Daten gelesen wurden. Der Validierungsablauf für dieses auch als BOCC+ bezeichnete Verfahren [36] ist in Abb. 12.4b skizziert. Dabei wird ein monoton wachsender Transaktionszähler TNC (transaction number count) verwendet, um einer erfolgreich validierten Transaktion eine Commit-Transaktionsnummer cts zuzuweisen. Diese wird als Änderungszeitstempel (write timestamp) wts für alle von der Transaktion geänderten Objekte verwendet. Beim Lesen eines Objekts wird zugleich die gesehene Versionsnummer (read timestamp) rts im Read-Set einer Transaktion vermerkt. Die Validierung erfordert damit lediglich die schnell ausführbare Überprüfung, ob die gesehenen Versionen noch aktuell sind.

Beispiel 12.6

Für den Schedule in Abb. 12.1a sei zunächst \(\textit{TNC}=0\) und \(\textit{wts}(y)=0\). Transaktion T 3 validiert für BOCC+ erfolgreich und seine Transaktionsnummer 1 wird der neuen Version von x zugewiesen: \(\textit{wts}(x)=1\). Diese Version wird von T 1 danach gelesen (\(\textit{rts}(x,T_{1})=1\)), sodass die Validierung von T 1 erfolgreich ist, da keine veraltete Objektversion gelesen wurde. Es wird zugleich eine neue Version von y mit Zeitstempel 2 erzeugt (\(\textit{wts}(y)=2\)). Damit wird bei der Validierung von T 2 festgestellt, dass eine veraltete Version von y (mit \(\textit{rts}(y,T_{2})=0\)) gelesen wurde, sodass T 2 abgebrochen wird. Es ergibt sich damit die Serialisierungsreihenfolge \(T_{3}<T_{1}\).  \(\square\)

In FOCC-Verfahren erfolgt keine Validierung gegenüber bereits beendeten, sondern gegenüber aktiven Transaktionen. Eine Validierung erfolgt nur für Änderungstransaktionen, wobei ein Konflikt besteht, falls eine in der Lesephase befindliche Transaktion ein Objekt aus dem Write-Set der Änderungstransaktion gelesen hat. Zur Konfliktauflösung können anstatt der validierenden Transaktion auch die betroffenen laufenden Transaktionen zurückgesetzt werden, um z. B. den Arbeitsverlust zu verringern. Unter anderem aufgrund dieser Flexibilität weisen FOCC-Ansätze in zentralen DBS Vorteile gegenüber BOCC auf [20]. Allerdings sind FOCC-Ansätze in verteilten Umgebungen aufgrund der Notwendigkeit der Validierung gegenüber laufenden Transaktionen nur schwer realisierbar [36], sodass sie praktisch keine Anwendung erreicht haben. Wir konzentrieren uns daher bei der weiteren Betrachtung optimistischer Ansätze auf BOCC-artige Validierungsansätze.

In Verteilten DBS kommen auch für die optimistischen Synchronisationsverfahren eine zentrale oder eine verteilte Realisierung in Betracht, die im Folgenden diskutiert werden.

4.2 Zentrale Validierung

Zur Durchführung der Validierungen sendet eine Transaktion (bei globalen Transaktionen die Primärtransaktion) am Transaktionsende den vollständigen Read- und Write-Set zu einem zentralen Knoten, der für die Durchführung der Validierungen verantwortlich ist. Dieser meldet dann nach der Validierung das Ergebnis zur (Primär-)Transaktion zurück, woraufhin entweder das Zurücksetzen der Transaktion oder die Schreibphase veranlasst wird. Bei globalen Transaktionen erfolgt die Schreibphase im Rahmen eines verteilten Commit-Protokolls, das auf ändernde Teiltransaktionen beschränkt werden kann.

Die zentrale Validierung hat neben der Einfachheit den Vorteil, dass zur eigentlichen Synchronisation nur eine Nachricht (die zur Validierung) erforderlich ist, auf die synchron gewartet werden muss. Damit liegt der Kommunikations-Overhead weit unter dem eines zentralen Sperrverfahrens, bei dem jede Sperranforderung eine synchrone Nachricht an den zentralen Lock-Manager verlangt. Zur Validierung kann nur eine BOCC-artige Synchronisation verwendet werden; die FOCC-Alternative scheitert daran, dass es unmöglich ist, im zentralen Knoten die aktuellen Read-Sets der in den verschiedenen Knoten laufenden Transaktionen zu kennen. Des Weiteren bestehen auch hier wieder die Probleme zentralisierter Lösungen vor allem hinsichtlich Verfügbarkeit sowie Knotenautonomie. Insbesondere ist selbst für lokale Transaktionen Kommunikation zur Validierung erforderlich.

4.3 Verteilte Validierung

Beim verteilten Validierungsschema wird jede (Sub-)Transaktion an ihrem ausführenden Rechner validiert. Damit wird für rein lokale Transaktionen, die idealerweise den größten Transaktionsanteil ausmachen, keine Kommunikation mit anderen Rechnern erforderlich. Auch für globale Transaktionen verursacht (wie schon bei verteilten Sperrverfahren) die Synchronisation keine zusätzlichen Nachrichten, da sich Validierungen und Schreibphasen im Rahmen eines Zwei-Phasen-Commit-Protokolls durchführen lassen:

  • Die PREPARE-Nachricht, welche die Primärtransaktion an alle Teiltransaktionen schickt, dient jetzt gleichzeitig als Validierungsaufforderung. Nach einer erfolgreichen lokalen Validierung sichert jede Teiltransaktion ihre Änderungen (Prepared-Zustand) und schickt eine READY-Nachricht zur Primärtransaktion. Bei gescheiterter lokaler Validierung dagegen wird eine FAILED-Nachricht zurückgesendet, und die Teiltransaktion setzt sich zurück. Bei der lokalen Validierung sind nun auch Prepared-Transaktionen.

  • Waren alle lokalen Validierungen erfolgreich, so verschickt die Primärtransaktion (nach Schreiben des Commit-Satzes) eine COMMIT-Nachricht an alle Teiltransaktionen, die Änderungen vorgenommen haben. Diese führen daraufhin ihre Schreibphasen durch. Verliefen eine oder mehrere der lokalen Validierungen erfolglos, leitet die Primärtransaktion durch Verschicken von ABORT-Nachrichten das Zurücksetzen der Transaktion ein.

Diese Vorgehensweise reicht jedoch allein nicht aus, um eine korrekte Synchronisation zu gewährleisten. Denn durch die lokalen Validierungen wird zwar die lokale Serialisierbarkeit in jedem Knoten sichergestellt, nicht aber notwendigerweise die globale Serialisierbarkeit, da die lokale Serialisierungsreihenfolge von Teiltransaktionen zweier globaler Transaktionen auf verschiedenen Rechnern entgegengesetzt sein kann. Zur Behandlung dieses Problems wurden mehrere Alternativen vorgeschlagen [36], wobei die wohl einfachste Lösung möglich wird, wenn die Validierungen globaler Transaktionen auf allen Knoten in der gleichen Reihenfolge ausgeführt werden. In diesem Fall entspricht die auf allen Rechnern gleiche Validierungsreihenfolge nicht nur der lokalen, sondern zugleich der globalen Serialisierungsreihenfolge.

Um die gleiche Validierungsreihenfolge auf allen Knoten zu erzwingen, können global eindeutige Transaktionszeitstempel verwendet werden. Diese Zeitstempel werden (im Gegensatz zu Zeitmarkenverfahren) beim EOT am Heimatknoten zugewiesen und bestimmen die Position der Transaktion in der globalen Serialisierungsreihenfolge. Der Transaktionszeitstempel wird bei den Validierungsaufforderungen mitgeschickt, sodass in jedem Knoten die gleiche Validierungsreihenfolge eingehalten werden kann. „Zu spät“ eintreffende Transaktionen, deren Zeitstempel kleiner ist als die von zuletzt lokal validierten Transaktionen, werden im einfachsten Verfahren zurückgesetzt. Das Ausmaß solcher Rücksetzungen ist umso geringer, je schneller und gleichmäßiger die Übertragungszeiten zwischen den Rechnern sind und je enger die lokalen Uhren der Rechner synchronisiert sind. Alternativ kann gemäß [1] bei einer bekannten maximalen Abweichung der lokalen Uhren eine genauere Konfliktprüfung gegenüber in der jüngeren Vergangenheit bereits beendeten Transaktionen erfolgen. Dabei würde eine verspätet eingetroffene Transaktion nur zurückgesetzt, wenn ein Objekt aus ihrem Write-Set von einer bereits beendeten Transaktion mit jüngerem Commit-Zeitstempel gelesen oder geändert wurde.

Ein weiteres Problem bei verteilter Validierung entsteht durch die zeitliche Trennung zwischen lokaler Validierung und Schreibphase. Denn im Gegensatz zu zentralisierten DBS ist nun auf einem Knoten nach erfolgreicher lokaler Validierung das Schicksal der globalen Transaktion ungewiss. Die von lokal erfolgreich validierten Teiltransaktionen beabsichtigten Änderungen sind demnach „unsicher“, da sie je nach Validierungsausgang der globalen Transaktion weggeworfen oder in die Datenbank eingebracht werden.

Beispiel 12.7

Im Schedule von Abb. 12.5 will Transaktion T 2 auf Objekt x zugreifen, das von der lokal bereits erfolgreich validierten, globalen Änderungstransaktion T 1 geändert wurde. Deren Änderung ist jedoch unsicher, da zu diesem Zeitpunkt ihr globales Commit-Ergebnis noch nicht bekannt ist.  \(\square\)

Abb. 12.5
figure 5

Probleme mit „unsicheren“ Änderungen

Für die Behandlung dieser unsicheren Änderungen in der Lesephase und bei der Validierung lokaler Transaktionen bietet sich nach [36] an, den Zugriff auf unsichere Änderungen zu blockieren, bis der Ausgang der globalen Transaktion feststeht. Da die Sperren nur während der Commit-Behandlung gehalten werden, ist die Wahrscheinlichkeit von Sperrkonflikten relativ gering im Vergleich zu reinen Sperrverfahren. Deadlocks können dabei nicht entstehen, da die Transaktion, auf die gewartet wird, ihre Lesephase bereits beendet hat und somit selbst nicht mehr auf solche Sperren laufen kann.

Mit diesem Ansatz können im Falle BOCC-artiger Synchronisation Validierungen und Schreibphasen quasi wie im zentralen Fall erfolgen. Wie in [39] beschrieben, können die Sperren weitergehend genutzt werden, um ein mehrfaches Zurücksetzen derselben Transaktion zu verhindern. Dabei werden die von einer Transaktion erworbenen Sperren im Falle einer gescheiterten Validierung beibehalten. Die Sperren sichern zu, dass die erneute Transaktionsausführung erfolgreich ist, sofern keine zusätzlichen Objekte referenziert werden. Es handelt sich dabei um eine spezielle Kombination von optimistischem Ansatz und Sperrverfahren. Deadlocks können verhindert werden, da die Sperranforderungen in allen Knoten in der Reihenfolge der Transaktionszeitstempel bearbeitet werden.

5 Mehrversionensynchronisation

Mehrversionensynchronisationsverfahren (multiversion concurrency control , MVCC ) streben eine Reduzierung an Synchronisationskonflikten an, indem für geänderte Objekte zeitweilig mehrere Versionen geführt werden und Lesezugriffe ggf. auf ältere Versionen zugreifen.

5.1 Grundlagen

Im einfachsten Fall der Mehrversionensynchronisation wird nur reinen Lesetransaktionen ein Zugriff auf potenziell ältere Objektversionen gewährt, während Änderungstransaktionen stets auf die aktuelle Version von Objekten zugreifen. Alternativ greifen bei Ansätzen der sogenannten Snapshot Isolation (SI) (Schnappschusssynchronisation) alle Transaktionen inklusive Änderungstransaktionen auf potenziell veraltete Versionen von Objekten zu.

Wir gehen erst später auf solche SI-Ansätze ein und betrachten zunächst den einfacheren Fall, in dem Änderungstransaktionen stets auf die aktuelle Version eines Objektes zugreifen und für deren Synchronisation praktisch jedes der allgemeinen Synchronisationsverfahren verwendet werden kann [2, 20]. Dabei wird vorausgesetzt, dass für jede Transaktion vorab Wissen darüber vorliegt, ob sie möglicherweise Änderungen vornimmt. Einer reinen Lesetransaktion T wird dabei während ihrer gesamten Laufzeit eine Sicht auf die Datenbank gewährt, wie sie bei ihrem BOT gültig war; Änderungen, die während ihrer Bearbeitung vorgenommen werden, bleiben für T unsichtbar. Um dies zu realisieren, erzeugt jede erfolgreiche Änderung eine neue Version des modifizierten Objekts; die Versionen werden in einem sogenannten Versionenpool verwaltet.

Da mit den Versionen jeder Lesetransaktion der bei BOT gültige (und konsistente) DB-Zustand (Schnappschuss) zur Verfügung gestellt wird, ist für Lesetransaktionen keinerlei Synchronisation mehr erforderlich. Weiterhin brauchen sich andere Transaktionen nicht mehr gegen Lesetransaktionen zu synchronisieren. Damit reduziert sich sowohl die Konfliktwahrscheinlichkeit (und damit die Anzahl von Blockierungen und Rücksetzungen) als auch der Synchronisierungsaufwand (Anzahl von Sperranforderungen, Validierungen etc.). Die Serialisierbarkeit bleibt generell gewahrt.

Beispiel 12.8

Für den Schedule in Abb. 12.1a trat mit einem RX-Protokoll ein Sperrkonflikt auf (siehe Beispiel 12.2). In Kombination mit einem Mehrversionenkonzept wird der Lesezugriff von T 2 auf das Objekt y nun ohne Konflikt abgewickelt, da der Zugriff auf die ungeänderte Version erfolgt. Die sich ergebende Serialisierungsreihenfolge lautet jetzt \(T_{2}<T_{3}<T_{1}\).  \(\square\)

Für die erheblichen Vorteile, die ein Mehrversionenkonzept bietet, muss zum einen in Kauf genommen werden, dass Lesetransaktionen (vor allem lange Leser) nicht immer die aktuellen Daten sehen. Zum anderen ist ein erhöhter Speicherplatzbedarf zur Haltung der Versionen sowie ein zusätzlicher Verwaltungsaufwand für den Versionenpool erforderlich.

5.1.1 Verwaltung eines Versionenpools

Bezüglich der Versionenpool -Verwaltung sind vor allem zwei Aufgaben zu behandeln, nämlich Bestimmung der zu lesenden Versionen sowie die Freigabe nicht mehr benötigter Versionen (garbage collection). Diese Aufgaben lassen sich relativ einfach durch Verwendung von Zeitstempeln lösen. Im zentralen Fall genügt dazu das Führen eines Transaktionszählers TNC. Beim Commit einer Änderungstransaktion wird der TNC inkrementiert und der Transaktion als Commit-Zeitstempel cts zugewiesen (ähnlich wie für BOCC+, Abb. 12.4b). Für jede Version eines geänderten Objektes wird ein Schreibzeitstempel wts geführt, der dem Commit-Zeitstempel der ändernden Transaktion entspricht. Für Lesetransaktionen wird dagegen beim Transaktionsbeginn der aktuelle TNC-Wert als BOT-Zeitstempel bts übernommen. Damit muss einer Lesetransaktion T für den Zugriff auf Objekt x die jüngste Version von x bereitgestellt werden, für die \(\textit{wts}(x)\leq bts(T)\) gilt. Änderungstransaktionen greifen stets auf die aktuellsten Objektversionen zu.

Um feststellen zu können, welche Versionen nicht mehr benötigt werden, wird der BOT-Zeitstempel der ältesten Lesetransaktion MinBts geführt. Eine Version x i von Objekt x kann gelöscht werden, falls es eine neuere Version x j gibt, sodass gilt:

$$\textit{wts}(x_{i})<\textit{wts}(x_{j})\leq\textit{MinBts}\> .$$

Beispiel 12.9

In obigem Beispiel (Schedule von Abb. 12.1a) seien folgende Initialwerte gegeben: \(\textit{wts}(x_{0})=0,\textit{wts}(y_{0})=0,\textit{TNC}=0\).

Die Lesetransaktion T 2 erhält den BOT-Zeitstempel \(bts(T_{2})=0\); zugleich wird MinBts auf diesen Wert gesetzt. Für den Zugriff auf y wird daher die ungeänderte Version y 0 mit \(\textit{wts}(y_{0})=0\) ausgewählt. Beim Commit von T 3 wird TNC auf 1 inkrementiert und als Commit-Zeitstempel für T 3 zugewiesen (\(\textit{cts}(T_{3})=1\)). Für die neue Version von Objekt x (x 1) gilt \(\textit{wts}(x_{1})=1\). Beim Commit von T 1 werden analog TNC auf 2 erhöht und eine neue Version y 1 mit \(\textit{wts}(y_{1})=2\) erzeugt. Am Ende von T 2 wird MinBts angepasst und überprüft, welche Versionen freigegeben werden können. Da zu diesem Zeitpunkt keine Lesetransaktion mehr läuft, wird MinBts auf den Wert \(\infty\) gesetzt. Die alten Versionen x 0 und y 0 werden freigegeben, da in beiden Fällen neuere Versionen existieren, deren Schreibzeitstempel kleiner-gleich MinBts sind.  \(\square\)

Leistungsuntersuchungen zeigten, dass mit solchen Mehrversionenverfahren die Leistungsfähigkeit in konfliktträchtigen Anwendungen signifikant verbessert werden kann [19, 9]. In mit realen DB-Lasten vorgenommenen Untersuchungen zeigte sich zudem, dass die überwiegende Mehrzahl der Objektzugriffe (\(> 90\,\%\)) auf die aktuelle Version und die Mehrzahl der restlichen Zugriffe auf die nächstältere Version entfallen [19]. Der Umfang des Versionenpools kann daher meist klein gehalten werden. Eine zunehmende Zahl relationaler DBS unterstützt daher einen Mehrversionenansatz, u. a. Oracle, MS SQL-Server und Postgres. Auch für In-Memory-DBS ist die reduzierte Konfliktwahrscheinlichkeit des Mehrversionensynchronisationsansatzes von großem Vorteil; zudem lässt sich durch die im Hauptspeicher zugänglichen Objekte mit ihren Versionsnummern eine sehr effiziente Implementierung erreichen [12, 27, 28].

5.1.2 Snapshot Isolation

In den realen Implementierungen wird meist die eingangs erwähnte MVCC-Variante der Snapshot Isolation  [6] unterstützt, bei der auch Änderungstransaktionen auf die bei Transaktionsbeginn gültigen Objektversionen oder auf die von ihnen selbst erzeugten Änderungen zugreifen. Die Synchronisation zwischen Änderungstransaktionen erfolgt dann ähnlich einer BOCC-artigen Synchronisation am Transaktionsende, wobei eine Änderungstransaktion T nur erfolgreich beendet werden kann, wenn ihr Write-Set disjunkt ist zu den Write-Sets der während der Laufzeit von T beendeten Transaktionen. Diese Strategie wird auch als First Commiter Wins bezeichnet. Mit diesem Ansatz werden zwar die meisten Mehrbenutzeranomalien vermieden, jedoch keine Serialisierbarkeit gewährleistet [6]. Eine in [8] vorgeschlagene Erweiterung (Serializable Snapshot Isolation ) prüft zusätzlich Lese-Schreib-Abhängigkeiten zwischen Änderungstransaktionen und kann dadurch Serialisierbarkeit erzielen. Eine Implementierung davon erfolgte in PostgresSQL  [35].

Bei der Übertragung einer Mehrversionensynchronisation auf verteilte Umgebungen ist vor allem zu klären, wie eine korrekte Vergabe von BOT- und Commit-Zeitstempeln gewährleistet werden kann, um Lesetransaktionen einen konsistenten DB-Schnappschuss zur Verfügung zu stellen. Hierfür bestehen wiederum zentralisierte als auch verteilte Realisierungsmöglichkeiten.

5.2 Mehrversionenansätze mit zentralisierter Kontrolle

Eine einfache Lösung dieses Problems besteht darin, den Zähler TNC auf einem dedizierten Knoten zu führen und die BOT- und Commit-Zeitstempel dort anzufordern. Der Commit-Zeitstempel wird dabei erst zugewiesen, nachdem das Durchkommen der Änderungstransaktion sichergestellt ist (nach Phase 1 des Commit-Protokolls). Er wird in der zweiten Commit-Phase allen Teiltransaktionen zur Vergabe der Versionsnummern für geänderte Objekte mitgeteilt. Am zentralen Knoten können auch für jeden Rechner die BOT-Zeitstempel der ältesten Lesetransaktionen (MinBts) vermerkt werden. Diese Angaben müssen nicht immer aktuell sein, sondern die Rechner können die betreffenden Änderungen asynchron (z. B. gebündelt mit Zeitmarkenanforderungen) mitteilen. Ebenso kann der zentrale Knoten das globale Minimum asynchron an die einzelnen Rechner weiterleiten, damit dort die nicht mehr benötigten Versionen ermittelt werden können.

Die Nutzung einer zentralisierten Zeitstempelverwaltung erfolgte bei der Realisierung einer Mehrversionensynchronisation für die Key Value Stores BigTable und HBase [23, 34]. Für BigTable wurde im Rahmen der Percolator -Implementierung [34] ein zentraler Timestamp-Service für die Zuteilung eindeutiger BOT- und Commit-Zeitstempel genutzt. Zur Synchronisation werden Zeitstempel sowie Sperren in eigenen Spalten in den vom BigTable-Subsystem geführten Tabellen verwendet. Die Synchronisation der Änderungstransaktionen erfolgt nicht zentralisiert, sondern verteilt im Rahmen des Zwei-Phasen-Commit. Dabei brechen sich Transaktionen in Phase 1 ab, wenn sie für beabsichtige Änderungen eine bereits erfolgte oder geplante Änderung mit jüngerem Zeitstempel feststellen. Für Lesetransaktionen entfällt das Commit-Protokoll, jedoch müssen sie ggf. auf die Freigabe einer noch in Vorbereitung befindlichen Änderung einer Transaktion mit kleinerem Commit-Zeitstempel als ihrem BOT-Zeitstempel warten.

Die HBase -basierte Implementierung von Snapshot Isolation in [23] ist dagegen vollständig zentralisiert. Neben der Vergabe der BOT- und Commit-Zeitstempel erfolgt auch die BOCC-artige Erkennung von Synchronisationskonflikten an einem zentralen Knoten. Hierzu wird einfach geprüft, ob für ein von einer Änderungstransaktion T gelesenes Objekt eine Version existiert, deren aktuelle Version jünger ist als der BOT-Zeitstempel von T. Wenn dies nicht der Fall ist, entspricht der BOT-Zustand der Datenbank (bis auf eigene Änderungen) dem bei EOT gültigen Zustand, d. h., es gibt keine Konflikte mit anderen Änderungstransaktionen. Anderenfalls wird die Änderungstransaktion abgebrochen.

5.3 Verteilte Mehrversionensynchronisation

Die zentralisierten Ansätze sind vergleichsweise einfach realisierbar, weisen jedoch wieder die bekannten Nachteile hinsichtlich Verfügbarkeit und Knotenautonomie auf. Zudem entstehen zusätzliche Kommunikationsverzögerungen zum Start und Abschluss von Transaktionen. Die Hauptschwierigkeit bei vollständig verteilten MVCC-Realisierungen liegt darin, Lesetransaktionen einen konsistenten DB-Schnappschuss zur Verfügung zu stellen, obwohl zur Zeitstempelvergabe keine zentralen, monoton wachsenden Zähler mehr geführt werden können. Die lokale Vergabe global eindeutiger Zeitstempel, bestehend aus lokaler Uhrzeit und Knoten-ID, ist problematisch, da nur innerhalb eines Rechners – nicht jedoch rechnerübergreifend – monoton wachsende Zeitstempel gewährleistet sind. Eine Folge davon ist, dass für eine Lesetransaktion mit lokal bestimmtem BOT-Zeitstempel t an anderen Rechnern eventuell noch nicht alle Versionen mit einem Zeitstempel kleiner als t erzeugt worden sind. Zudem gibt es wiederum das Problem unsicherer Änderungen aufgrund verteilter Änderungstransaktionen. Schließlich müssen Rechner eventuell viele alte Objektversionen vorhalten, um Anforderungen externer Transaktionen mit sehr altem BOT-Zeitstempel bedienen zu können.

Beispiel 12.10

Im Schedule von Abb. 12.6 wird die globale Lesetransaktion T 1 in Rechner 1 zum lokalen Zeitpunkt t gestartet. Ihre Teiltransaktion in Rechner 2 startet zum Zeitpunkt \(t^{\prime}\), der aufgrund unsynchronisierter Uhrzeiten jedoch vor t liegt. Somit kann in Rechner 2 eine (lokale) Änderungstransaktion T 2 noch vor dem lokalen Zeitpunkt t die für die Transaktion T 1 relevante Änderung von Objekt z vornehmen. Es muss daher entweder der Lesezugriff \(r(z)\) für T 1 bis zum Zeitpunkt t auf Rechner 2 verzögert werden oder T 2 müsste einen größeren Commit-Zeitstempel als t erhalten.  \(\square\)

Abb. 12.6
figure 6

MVCC-Problem mit unsynchronisierter Zeitstempelvergabe

Die effektive Realisierung einer verteilten Mehrversionensynchronisation erfordert eine Synchronisation der lokalen Uhrzeiten bzw. Zeitstempelvergabe. Hierzu können z. B. Protokolle zur Synchronisation der physischen Uhrzeiten eingesetzt werden, die eine maximale Abweichung der Uhren um eine bestimmte, kleine Zeitspanne garantieren, z. B. das Network Time Protocol (www.ntp.org). Alternativ kann analog zu dem Lamport-Ansatz [26] eine Verwendung logischer Uhrzeiten eingesetzt werden, die sich auf Basis zwischen den Rechnern ohnehin ausgetauschter Nachrichten synchronisieren. So kann bei der Vergabe der Commit-Zeitstempel darauf geachtet werden, dass diese größer sind als die BOT-Zeitstempel aller zuvor an einem Rechner ausgeführten (externen) Lesetransaktionen (damit würde das Problem in obigem Beispiel ohne Verzögerung für die Lesetransaktion behandelt werden). Weiterhin muss ein lokal vergebener Commit-Zeitstempel größer sein als die Commit-Zeitstempel, die zuvor aufgrund von verteilten Commit-Ausführungen globaler Änderungstransaktionen bekannt wurden.

Eine mögliche Realisierung sieht vor, für eine globale Änderungstransaktion T bei Transaktionsende am Heimat- oder Koordinatorknoten zunächst nur einen vorläufigen, lokalen Commit-Zeitstempel (precommit timestamp ), \(\textit{pcts}(T)\), festzulegen, der in Phase 1 des Commit-Protokolls an die beteiligten Rechner propagiert wird. Der endgültige Commit-Zeitstempel wird nach Rückmeldung der beteiligten Knoten bestimmt, sodass er größer ist als alle an den involvierten Rechnern bekannten BOT- und Commit-Zeitstempel. Der endgültige Commit-Zeitstempel, \(\textit{cts}(T)\), wird in der zweiten Commit-Phase an die beteiligten Rechner mitgeteilt.

Eine solche Vergabe von Commit-Zeitstempeln hat Auswirkungen auf Lesetransaktionen, da die Auswahl einer Objektversion vom endgültigen Commit-Zeitstempel abhängt. Weiterhin sind die Änderungen einer Transaktion, die noch keinen endgültigen Commit-Zeitstempel besitzt, unsicher, da die Transaktion möglicherweise noch abgebrochen wird. Die unsicheren Änderungen einer globalen Änderungstransaktion T mit vorläufigem Commit-Zeitstempel pcts sind relevant für Lesetransaktionen T j , für die gilt: \(bts(T_{j})> \textit{pcts}(T)\). Hier empfiehlt es sich wiederum, die Lesezugriffe so lange zu blockieren, bis der Transaktionsausgang sowie der endgültige Commit-Zeitstempel für T feststeht.

Zur Freigabe von alten Objektversionen muss der BOT-Zeitstempel der ältesten Lesetransaktion im System bekannt sein. Hierzu ist es bei verteilter Realisierung des Mehrversionenansatzes erforderlich, dass jeder Knoten seinen lokalen MinBts-Wert systemweit propagiert (z. B. gebündelt mit Nachrichten für das Commit-Protokoll). Damit kann in jedem Rechner eine Approximation des globalen Minimums geführt werden.

Eine verteilte Mehrversionensynchronisation wurde u. a. in Googles verteiltem Datenbanksystem Spanner realisiert, das eine Skalierbarkeit für Millionen von Knoten unter Einhaltung der ACID-Eigenschaften anstrebt [10, 29]. Es wird dabei eine enge Uhrensynchronisation durch Einsatz von Atomuhren und GPS mit einer maximalen Abweichung Eps von unter 10 ms unterstützt. Reine Lesetransaktionen lesen damit an jedem Knoten die zu ihrem BOT-Zeitpunkt gültige Version, wobei eine Verzögerung um die maximal mögliche Zeitabweichung Eps in Kauf genommen wird. Änderungstransaktionen werden im Rahmen des Commit-Protokolls mit Sperren synchronisiert. Die Sperren werden dabei nur für die Dauer der Commit-Verarbeitung gehalten, wodurch die Wahrscheinlichkeit von Konflikten klein gehalten wird. Die Commit-Zeitmarke einer verteilten Transaktion ist größer oder gleich dem Maximum der von jeder Teiltransaktion kommenden Prepare-Zeitpunkte. In [13] wird ein alternativer Ansatz zur verteilten Mehrversionensynchronisation mit synchronisierten Uhren vorgestellt. Dabei wird für Änderungstransaktionen eine verteilte BOCC-artige Synchronisation zur Sicherstellung der Snapshot Isolation realisiert, ähnlich wie in Abschn. 12.4.3 besprochen.

6 Deadlock-Behandlung

Eine mit Sperrverfahren einhergehende Interferenz ist die Gefahr von Verklemmungen oder Deadlocks, deren charakterisierende Eigenschaft eine zyklische Wartebeziehung zwischen zwei oder mehr Transaktionen ist. Im verteilten Fall können diese Verklemmungen zwischen Transaktionen verschiedener Rechner auftreten, sodass es zu sogenannten globalen Deadlocks kommt.

Beispiel 12.11

In einer Bankanwendung kann es zu einem globalen Deadlock zwischen zwei Überweisungstransaktionen kommen, die auf dieselben Konten K 1 (an Rechner R 1) und K 2 (an Rechner R 2) zugreifen wollen (Abb. 12.7). Dabei beabsichtigt die in R 1 gestartete Transaktion T 1 eine Überweisung von K 1 nach K 2, die Transaktion T 2 in R 2 eine Überweisung von K 2 nach K 1. T 1 erwirbt zunächst eine Schreibsperre auf K 1 und führt die Kontoänderung durch, danach startet sie eine Teiltransaktion in R 2, um auf K 2 zuzugreifen. Die entsprechende Sperranforderung führt jedoch zu einem Konflikt, da T 2 bereits eine Schreibsperre auf K 2 erworben hat. T 2 hat unterdessen eine Teiltransaktion in R 1 gestartet, um Konto K 1 zu ändern; diese Teiltransaktion gerät jedoch in einen Konflikt mit T 1. Beide Transaktionen blockieren sich nunmehr gegenseitig; die Situation kann nur durch Rücksetzung einer der Transaktionen behoben werden.  \(\square\)

Abb. 12.7
figure 7

Beispiel eines globalen Deadlocks

Zur Deadlock-Behandlung in zentralisierten sowie in Verteilten DBS kommen einige generelle Strategien in Betracht: Verhütung, Vermeidung, Timeout, Erkennung sowie hybride Strategien. Diese Alternativen werden im Folgenden näher diskutiert. Besonderheiten für Verteilte DBS ergeben sich vor allem bezüglich der Deadlock-Erkennung.

6.1 Deadlock-Verhütung und -Vermeidung

Die Deadlock-Verhütung (prevention) ist dadurch gekennzeichnet, dass die Entstehung von Deadlocks verhindert wird, ohne dass dazu Maßnahmen während der Abarbeitung und dem Objektzugriff der Transaktionen erforderlich sind. In diese Kategorie fallen v. a. die sogenannten Preclaiming -Sperrverfahren, bei denen eine Transaktion alle benötigten Sperren bereits bei BOT anfordern muss. Verklemmungen können dabei umgangen werden, indem jede Transaktion ihre Sperren in einer global festgelegten Reihenfolge anfordert, da dann keine zyklischen Wartebeziehungen entstehen können. Für Beispiel 12.11 müssten so beide Transaktionen die Konten in derselben Reihenfolge sperren, z. B. zunächst K 1 und dann K 2. Durch den Sperrkonflikt für K 1 würde die zweite Transaktion blockiert werden, bis die erste Transaktion beendet wird und die Sperre freigibt. Ein Deadlock und eine Rücksetzung würden somit verhindert.

Das Kernproblem der Deadlock-Verhütung ist jedoch, dass bei Beginn einer Transaktion i. Allg. nur Obermengen der zu referenzierenden Objekte (z. B. ganze Relationen) bekannt sind. Der somit verursachte Grad an Sperrkonflikten ist jedoch in der Regel inakzeptabel. Im verteilten Fall kommt als weiterer Nachteil hinzu, dass für nicht lokal verwaltete Objekte vor Transaktionsbeginn eigene Kommunikationsvorgänge für die Sperranforderungen erforderlich sind. Wegen dieser Schwächen hat der Preclaiming-Ansatz für DBS keine praktische Relevanz. Wir gehen daher im Weiteren davon aus, dass Sperren stets während der Transaktionsverarbeitung (unmittelbar vor einem Objektzugriff) angefordert werden.

Bei der Deadlock-Vermeidung (avoidance) werden potenzielle Deadlocks im Voraus erkannt und durch entsprechende Maßnahmen vermieden; im Gegensatz zur Verhütung ist also eine Laufzeitunterstützung zur Deadlock-Behandlung erforderlich. Die Vermeidung der Deadlocks erfolgt generell durch Zurücksetzen von möglicherweise betroffenen Transaktionen. Ein einfacher Vermeidungsansatz wäre, im Falle eines Sperrkonfliktes die in Konflikt geratene Transaktion bzw. den Sperrbesitzer abzubrechen, sodass keine Blockierungen auftreten. Dieser „Immediate Restart“-Ansatz scheidet jedoch aufgrund der hohen Anzahl von Rücksetzungen aus. Wir betrachten stattdessen Alternativen, welche weniger Rücksetzungen verursachen und Zeitmarken bzw. Zeitintervalle zur Deadlock-Vermeidung verwenden.

6.1.1 Wait/Die und Wound/Wait

Zwei einfache Verfahren zur Deadlock-Vermeidung sind Wait/Die und Wound/Wait  [37]. Bei ihnen wird wie für Zeitmarkenverfahren (Abschn. 12.3) jeder Transaktion T bei BOT eine global eindeutige Zeitmarke \(\textit{ts}(T)\) zugewiesen, wobei hier die Kombination aus lokaler Uhrzeit und Knoten-ID ausreicht. Im Gegensatz zu den Zeitmarkenverfahren erfolgt jetzt die Synchronisation jedoch mit einem Sperrprotokoll. Die Transaktionszeitmarken werden lediglich im Falle eines Sperrkonfliktes herangezogen, um Deadlocks zu vermeiden. Dies kann erreicht werden, wenn vorgeschrieben wird, dass bei einem Sperrkonflikt stets nur die ältere (bzw. die jüngere) der beteiligten Transaktionen warten darf und anderenfalls der Konflikt durch Rücksetzung einer Transaktion aufgelöst wird. Denn dann kann sich keine zyklische Wartebeziehung mehr bilden.

Die bei Wait/Die bzw. Wound/Wait verwendete Behandlung von Sperrkonflikten zeigt Abb. 12.8. Im Wait/Die-Verfahren (Abb. 12.8 oben) darf die die Sperre anfordernde Transaktion T i nur dann warten, wenn sie älter ist als die Transaktion T j , welche die Sperre besitzt. Ist dagegen T i jünger als T j , so muss T i „sterben“ (die), d. h., sie wird zurückgesetzt. Damit warten bei diesem Ansatz stets ältere Transaktionen auf jüngere, jedoch nicht umgekehrt. Im Wound/Wait-Verfahren (Abb. 12.8 unten) dagegen warten stets jüngere Transaktionen auf ältere. Ist die anfordernde Transaktion T i älter als der Sperrbesitzer T j , so wird jedoch nicht die anfordernde Transaktion, sondern der Sperrbesitzer abgebrochen („verwundet“, wound). Es handelt sich dabei also um einen preemptiven AnsatzFootnote 1. Beide Strategien vermeiden Deadlocks ohne zusätzliche Kommunikationsvorgänge, da die Zeitstempel lokal zugewiesen und die Zeitstempelvergleiche im Konfliktfall lokal durchgeführt werden können. Es kann allerdings zu Rücksetzungen kommen, ohne dass ein Deadlock vorliegt.

Abb. 12.8
figure 8

Konfliktbehandlung bei Wait/Die und Wound/Wait (Sperranforderung von T i gerät in Konflikt mit T j )

Beispiel 12.12

Für den Schedule in Abb. 12.1a tritt mit einem RX-Protokoll ein Sperrkonflikt auf (Beispiel 12.2), es liegt jedoch kein Deadlock vor. Für Wait/Die wird der Sperrkonflikt dennoch durch Rücksetzung von T 2 aufgelöst, da T 2 jünger als T 1 ist. Für Wound/Wait dagegen wartet T 2 auf die Sperrfreigabe durch T 1.  \(\square\)

Beispiel 12.13

Für den in Abb. 12.7 gezeigten Deadlock (Beispiel 12.11) sei T 1 älter als T 2, d. h., \(\textit{ts}(T_{1})<\textit{ts}(T_{2})\). Bei Wait/Die wartet T 1 beim Sperrkonflikt in Rechner R 2, da sie älter als T 2 ist. Dagegen muss T 2 beim Sperrkonflikt in R 1 sterben. Bei Wound/Wait wird beim Sperrkonflikt von T 1 in R 2 der jüngere Sperrbesitzer T 2 verwundet, also zurückgesetzt. In beiden Fällen wird der Deadlock also durch Rücksetzen der jüngeren Transaktion T 2 aufgelöst.  \(\square\)

Bei Wait/Die und Wound/Wait werden im Gegensatz zu den Zeitmarkenverfahren (Abschn. 12.3) ältere Transaktionen bevorzugt, da im Falle einer Rücksetzung stets die jüngere der am Konflikt beteiligten Transaktionen zurückgesetzt wird. Daher empfiehlt es sich generell, bei Wait/Die und Wound/Wait nach einer Rücksetzung die ursprüngliche Zeitmarke beizubehalten, um die Wahrscheinlichkeit weiterer Rücksetzungen zu begrenzen. Die Bevorzugung älterer Transaktionen ist besonders ausgeprägt bei Wound/Wait, wo sogar der Sperrbesitzer abgebrochen wird, falls er jünger ist als die anfordernde Transaktion. Bei Wait/Die ist das Durchkommen älterer Transaktionen auch gesichert, jedoch nimmt mit zunehmendem Alter die Wahrscheinlichkeit von Blockierungen zu. Bei Wait/Die sollte eine zurückgesetzte Transaktion T 2 auch erst dann wieder gestartet werden, wenn die in Konflikt stehende, ältere Transaktion T 1 beendet ist. Denn anderenfalls würde beim erneuten Zugriff auf das betreffende Objekt wiederum ein Abbruch von T 2 erfolgen. Diese Gefahr besteht bei Wound/Wait nicht, da hier der (jüngere) Sperrbesitzer zurückgesetzt wird. Bei einem erneuten Zugriff auf das Objekt während der Wiederausführung erfolgt für die jüngere, anfordernde Transaktion jedoch keine Rücksetzung, sondern sie kann auf die Sperrfreigabe warten.

In der Simulationsstudie [4] zeigte Wound/Wait ein konsistent besseres Leistungsverhalten als Wait/Die. Der Wound/Wait-Ansatz wird in Google Spanner zur Deadlock-Vermeidung genutzt [10].

6.1.2 Dynamische Zeitmarken und Zeitintervalle

Wait/Die und Wound/Wait basieren beide auf statischen Zeitmarken, da sie während der Ausführung der Transaktion nicht mehr geändert werden. In [5] wurde die Verwendung dynamischer Zeitmarken vorgeschlagen, wobei Transaktionen zunächst ohne Zeitmarke gestartet werden. Erst wenn der erste Konflikt mit einer anderen Transaktion erfolgt, wird eine Transaktionszeitmarke bestimmt. Dabei kann stets eine Zeitmarke gewählt werden, sodass der Konflikt ohne Rücksetzung überstanden wird. Damit überlebt eine Transaktion zumindest den ersten Sperrkonflikt, womit die Anzahl von Rücksetzungen gegenüber der Verwendung statischer Zeitmarken reduziert wird.

Beispiel 12.14

Für den Schedule in Abb. 12.1a verursachte Wait/Die die Rücksetzung von T 2, obwohl kein Deadlock vorlag (Beispiel 12.13). Diese Rücksetzung kann mit dynamischen Zeitmarken vermieden werden, da die Zuordnung der Zeitmarken für T 1 sowie T 2 erst vorgenommen wird, wenn es aufgrund des Lesezugriffs von T 2 zum Sperrkonflikt mit T 1 kommt. Im Falle von Wait/Die weist man T 2 zu diesem Zeitpunkt eine kleinere Zeitmarke als T 1 zu, sodass T 2 auf die Sperre warten kann, anstatt zurückgesetzt zu werden.  \(\square\)

Eine Verallgemeinerung der Nutzung dynamischer Zeitmarken stellt die Verwendung von Zeitintervallen dar [5]. Dabei wird jeder Transaktion anstelle einer Zeitmarke ein Zeitintervall zugeordnet, das die mögliche Position der Transaktion in der Serialisierungsreihenfolge eingrenzt. Das zunächst unendliche Zeitintervall wird bei jedem Sperrkonflikt dynamisch verkleinert. Ein leeres Zeitintervall, das eine Rücksetzung erzwingt, ergibt sich für eine Transaktion erst bei einem Sperrkonflikt mit einer Transaktion mit disjunktem Zeitintervall, welches in falscher zeitlicher Relation zum eigenen Intervall steht. Durch geschickte Wahl der Zeitintervalle kann das Ausmaß unnötiger Rücksetzungen jedoch i. Allg. geringer als mit statischen oder dynamischen Zeitintervallen gehalten werden [31]. Dafür ergibt sich eine höhere Komplexität der Realisierung.

6.2 Timeout-Verfahren

Bei diesem sehr einfachen und billigen Verfahren wird eine Transaktion zurückgesetzt, sobald ihre Wartezeit auf eine Sperre eine festgelegte Zeitschranke (Timeout) überschreitet. Da ein Deadlock von alleine nicht verschwindet, wird irgendwann der Timeout überschritten, sodass jeder Deadlock aufgelöst wird. Das Hauptproblem mit diesem Ansatz ist die geeignete Wahl des Timeout-Wertes. Wird er hoch angesetzt, werden Deadlocks erst nach längerer Zeit aufgelöst, sodass die betroffenen Transaktionen unnötig lange blockiert sind. Ein kleiner Wert dagegen kann zu einer hohen Anzahl unnötiger Rücksetzungen von Transaktionen führen, ohne dass also ein Deadlock vorliegt. Ein akzeptabler Mittelwert hängt von vielen sich ändernden Faktoren ab wie der Lastzusammensetzung, Konfliktwahrscheinlichkeit und der Verfügbarkeit und Auslastung der Rechner und Kommunikationsverbindungen.

Mehrere Leistungsstudien zeigten, dass der Timeout-Ansatz nur im Falle geringer Konflikthäufigkeit hinreichend stabil ist, ansonsten jedoch eine sehr hohe Anzahl von Rücksetzungen verursacht [21, 4]. Dennoch wird der Timeout-Ansatz aufgrund seiner einfachen Realisierbarkeit in vielen kommerziellen DBS eingesetzt.

6.3 Deadlock-Erkennung

Bei der Deadlock-Erkennung (detection) werden sämtliche Wartebeziehungen aktiver Transaktionen explizit in einem Wartegraphen (wait-for graph ) protokolliert und Verklemmungen durch Zyklensuche in diesem Graphen erkannt. Im Gegensatz zum Serialisierungs- oder Abhängigkeitsgraphen sind im Wartegraphen nur aktive Transaktionen berücksichtigt, die an einem Sperrkonflikt beteiligt sind. Die Auflösung eines Deadlocks geschieht durch Rücksetzung einer oder mehrerer der am Zyklus beteiligten Transaktionen. Die Deadlock-Erkennung ist zwar im Vergleich zu Vermeidungs- oder Timeout-Strategien am aufwendigsten, dafür kommt sie mit den wenigsten Rücksetzungen aus. Dies bewirkte in quantitativen Untersuchungen [4] die beste Leistungsfähigkeit für zentralisierte DBS, wo die Deadlock-Erkennung auch relativ einfach realisiert werden kann [22, 3].

Beispiel 12.15

Abbildung 12.9 zeigt ein Beispiel eines Wartegraphen. Die Kanten zwischen den Transaktionen sind mit dem Objekt gekennzeichnet, für das der Konflikt auftrat. Für die Sperranforderung von T 2 auf Objekt O 1 liegt ein Konflikt mit zwei Transaktionen vor, z. B. aufgrund gewährter Lesesperren. In dem gezeigten Szenario liegen zwei Deadlocks vor: \(T_{1}\rightarrow T_{2}\rightarrow T_{3}\rightarrow T_{4}\rightarrow T_{1}\) sowie \(T_{1}\rightarrow T_{2}\rightarrow T_{5}\rightarrow T_{1}\). Die Rücksetzung von T 2 würde beide Deadlocks auflösen.  \(\square\)

Abb. 12.9
figure 9

Beispiel eines Wartegraphen

Die Zyklensuche kann bei jedem Sperrkonflikt vorgenommen werden (continous deadlock detection) oder in periodischen Zeitabständen. Im ersteren Fall wird ein Deadlock zum frühestmöglichen Zeitpunkt aufgelöst; zudem vereinfacht sich die Suche, da i. Allg. nur ein Teil des Graphen zu berücksichtigen ist. Bei der periodischen Zyklensuche ist stets der ganze Graph zu durchsuchen, dafür kann der Aufwand durch eine seltenere Ausführung begrenzt werden. Allerdings ergibt sich dann wieder eine verzögerte Auflösung von Deadlocks ähnlich wie bei einem Timeout-Ansatz. Bei der Auswahl der „Opfer“ können verschiedene Kriterien herangezogen werden, z. B. Minimierung des Arbeitsverlustes oder Einfachheit der Opferbestimmung. Wird bei einem Sperrkonflikt jeweils sofort eine Deadlock-Erkennung vorgenommen, kann ein neuer Deadlock einfach durch Rücksetzung der in den Konflikt geratenen Transaktion umgangen werden. Dies ist im verteilten Fall jedoch nicht mehr möglich.

Die Deadlock-Erkennung in Verteilten DBS ist weitaus aufwendiger als in zentralisierten DBS, da Kommunikationsvorgänge zur Mitteilung von Wartebeziehungen notwendig werden. Zudem ist es schwierig, überhaupt eine korrekte Deadlock-Erkennung zu realisieren, da jeder Knoten nur bezüglich lokaler Transaktionen die aktuelle Wartesituation kennt. Warteinformationen bezüglich externer Transaktionen sind dagegen aufgrund zeitlicher Verzögerungen bei der Übertragung i. Allg. veraltet. So konnte für viele Verfahrensvorschläge gezeigt werden, dass sie „falsche“ Deadlocks (Phantom-Deadlocks) erkennen und somit Transaktionen zurücksetzen, ohne dass ein Deadlock vorliegt [7]. Vielfach werden auch globale Deadlocks mehrfach erkannt, was ebenfalls zu unnötigen Rücksetzungen führt.

Im Folgenden diskutieren wir verschiedene zentrale und verteilte Ansätze zur globalen Deadlock-Erkennung.

6.3.1 Zentrale Deadlock-Erkennung

Hierbei wird in einem zentralen Knoten ein globaler Wartegraph geführt und auf Zyklen durchsucht. Um den Kommunikations-Overhead einzugrenzen, schickt jeder Rechner die bei ihm entstehenden Wartebeziehungen in periodischen Zeitabständen an den zentralen Knoten (Nachrichtenbündelung). Dieser vervollständigt damit seinen Wartegraph und startet die Zyklensuche. Da der globale Wartegraph wegen der Nachrichtenverzögerungen i. Allg. nicht auf dem aktuellsten Stand ist, werden Deadlocks nicht nur verspätet entdeckt, es können auch Phantom-Deadlocks erkannt und damit unnötige Rücksetzungen verursacht werden. Die Sonderrolle des für die Deadlock-Erkennung zuständigen Rechners bewirkt zudem ähnliche Probleme bezüglich Verfügbarkeit und Rechnerautonomie wie bei einem zentralen Sperrverfahren. Nach einem Ausfall des zentralen Knotens könnte jedoch leicht ein anderer dessen Funktion übernehmen bzw. auf ein Timeout-Verfahren umgestellt werden.

Eine zentrale Deadlock-Erkennung wurde in Distributed Ingres vorgenommen [38].

6.3.2 Verteilte Deadlock-Erkennung

Eine verteilte Deadlock-Erkennung vermeidet die Abhängigkeiten zu einem zentralen Knoten, ist dafür jedoch wesentlich schwieriger zu realisieren. Insbesondere kann es zur Mehrfacherkennung globaler Deadlocks kommen und Warteinformationen sind ggf. über mehrere Rechner zu propagieren, bis ein globaler Deadlock erkannt wird. Ein Überblick zu den zahlreichen Verfahrensvorschlägen findet sich in [14, 24]. Wir beschränken uns hier auf die Beschreibung des im Prototyp R* implementierten Verfahrens von Obermarck [30, 32]. Es hat den großen Vorteil, dass lediglich eine Nachricht benötigt wird, um globale Deadlocks aufzulösen, an denen zwei Rechner beteiligt sind. Dies ist der wichtigste Typ von globalen Deadlocks, da typischerweise die meisten Deadlocks (\(> 90\,\%\)) nur Zykluslänge 2 aufweisen [17]. Eine zentrale Deadlock-Erkennung hätte bereits zur Übertragung der Wartebeziehungen einen höheren Aufwand erfordert.

Das Obermarck-Verfahren geht davon aus, dass in jedem Rechner ein spezieller Prozess („deadlock detector“) mit der Deadlock-Erkennung beauftragt ist und mit den entsprechenden Prozessen der anderen Rechner kommuniziert. Jeder dieser Prozesse führt einen lokalen Wartegraphen, in dem sämtliche Wartebeziehungen lokaler und externer Transaktionen hinsichtlich der an diesem Rechner verwalteten Objekte geführt werden. Damit lassen sich lokale Deadlocks sofort und ohne Kommunikation erkennen. Im Wartegraphen wird ein spezieller Knoten EXTERNAL geführt, der Wartebeziehungen zu Teiltransaktionen auf anderen Rechnern kennzeichnet. Für eine externe Teiltransaktion T j wird stets eine Wartebeziehung \(\textup{{{EXTERNAL}}}\rightarrow T_{j}\) aufgenommen, da möglicherweise an anderen Rechnern auf Sperren von T j gewartet wird. Eine Wartebeziehung \(T_{i}\rightarrow\textup{{{EXTERNAL}}}\) wird ferner eingeführt, sobald Transaktion T i eine externe Teiltransaktion startet, da diese möglicherweise in einen Konflikt gerät. Ein potenzieller globaler Deadlock wird durch einen Zyklus im lokalen Wartegraphen angezeigt, an dem der EXTERNAL-Knoten beteiligt ist:

$$\textup{{{EXTERNAL}}}\rightarrow T_{1}\rightarrow T_{2}\rightarrow{\dots}\rightarrow T_{n}\rightarrow\textup{{{EXTERNAL}}}\> .$$

Um festzustellen, ob tatsächlich ein globaler Deadlock vorliegt, wird die Warteinformation an den Rechner weitergeleitet, an dem Transaktion T n die Teiltransaktion gestartet hatte. Nach Empfang solcher Warteinformationen vervollständigt der zuständige Prozess seinen Wartegraphen und führt darauf eine Zyklensuche durch. Zur Erkennung eines globalen Deadlocks kann dazu erneut die Weiterleitung der erweiterten Warteinformation an einen anderen Rechner erforderlich sein. Wird ein vollständiger Zyklus festgestellt, erfolgt die Rücksetzung einer der beteiligten Transaktionen. Dabei sollte, wenn möglich, eine lokale Transaktion ausgewählt werden, um den Deadlock möglichst schnell zu beheben [30].

Ein Problem mit dem skizzierten Verfahren liegt darin, dass sich im Falle eines globalen Deadlocks an jedem der beteiligten Rechner ein Zyklus mit dem EXTERNAL-Knoten ergibt. Wenn jetzt jeder dieser Rechner seine Warteinformation zur globalen Deadlock-Erkennung wie oben beschrieben weitergibt, führt dies jedoch zu einem unnötig hohen Kommunikationsaufwand sowie zur mehrfachen Erkennung eines globalen Deadlocks. Zur Abschwächung dieses Problems wird jeder Transaktion T wiederum eine global eindeutige Zeitmarke \(\textit{ts}(T)\) mitgegeben. Die Weiterleitung der Warteinformation in obiger Situation wird damit nur dann vorgenommen, wenn \(\textit{ts}(T_{n})<\textit{ts}(T_{1})\) gilt. Damit wird bei einem globalen Deadlock mit zwei Rechnern gewährleistet, dass nur einer der beiden Rechner die Warteinformation weiterleitet. Bei mehr als zwei Rechnern ist es jedoch weiterhin möglich, dass ausgehend von mehr als einem Rechner die Warteinformation weitergegeben wird.

Beispiel 12.16

Für den in Abb. 12.7 gezeigten Deadlock (Beispiel 12.11) bildet sich in Rechner R 1 folgender Zyklus:

$$\textup{{{EXTERNAL}}}\rightarrow T_{2}\rightarrow T_{1}\rightarrow\textup{{{EXTERNAL}}}\> ,$$

in R 2 dagegen

$$\textup{{{EXTERNAL}}}\rightarrow T_{1}\rightarrow T_{2}\rightarrow\textup{{{EXTERNAL}}}\> .$$

Wenn \(\textit{ts}(T_{1})<\textit{ts}(T_{2})\) gilt, dann sendet lediglich R 1 seine Warteinformation an R 2, nicht jedoch umgekehrt. In R 2 wird der Wartegraph vervollständigt und der Zyklus \(T_{1}\rightarrow T_{2}\rightarrow T_{1}\) entdeckt. Der Deadlock wird durch Rücksetzen der lokalen Transaktion T 2 aufgelöst.  \(\square\)

Der Obermarck-Algorithmus benötigt maximal \(N\cdot(N-1)/2\) Nachrichten für einen sich über N Rechner erstreckenden globalen Deadlock. Für N = 2 fällt also lediglich eine Nachricht an. Allerdings kann es zur Erkennung „falscher“ Deadlocks kommen, da die Wartegraphen der einzelnen Rechner i. Allg. unterschiedliche Änderungszustände reflektieren [14].

6.4 Hybride Strategien

Die Diskussion der vorgestellten Verfahren zur Deadlock-Behandlung zeigt, dass alle Vor- und Nachteile aufweisen. So sprechen vor allem Gründe der Einfachheit sowie das Umgehen zusätzlicher Nachrichten für eine Deadlock-Vermeidung oder einen Timeout-Ansatz. Diese verursachen jedoch u. U. eine hohe Anzahl unnötiger Rücksetzungen. Erkennungsansätze dagegen erlauben i. d. R. die geringste Anzahl von Rücksetzungen, sind jedoch schwierig zu realisieren und führen Kommunikationsaufwand ein. Vielversprechend erscheinen daher hybride Ansätze, um die Vorteile der einzelnen Verfahrensklassen zu vereinen.

Ein möglicher Ansatz hierzu sieht eine explizite Erkennung von lokalen Deadlocks vor, die also nur Objekte eines Rechners betreffen. Zur Behandlung globaler Deadlocks dagegen wird auf einen einfachen und billigen Vermeidungs- oder Timeout-Ansatz zurückgegriffen. Eine solche Vorgehensweise bewahrt den Vorteil der Einfachheit und vermeidet Kommunikation zur Deadlock-Behandlung. Außerdem ist davon auszugehen, dass die meisten Deadlocks nur Transaktionen eines Rechners berühren (und somit explizit erkannt werden), da ein Deadlock wie erwähnt meist nur zwei Transaktionen betrifft und häufig eine hohe Lokalität im Referenzverhalten erreicht wird. Daher ist eine hybride Strategie z. B. mit einem Timeout-Ansatz zur Auflösung globaler Deadlocks umso angebrachter, je höher die erzielbare Lokalität ist.

7 Übungsaufgaben

Übung 12.1 (Schmutzige vs. unsichere Änderungen)

Erläutern Sie den Unterschied zwischen schmutzigen und unsicheren Änderungen. Wieso ist bei optimistischer Synchronisation kein Zugriff auf schmutzige Änderungen möglich?

Übung 12.2 (Vergleich von Synchronisationsverfahren)

An einem Rechner sei folgender Schedule von drei Transaktionen mit Zugriff auf ausschließlich lokale Objekte gegeben, wenn keine Synchronisation erfolgt:

figure a

Bestimmen Sie für jedes der folgenden Synchronisationsverfahren die vorkommenden Blockierungen und Rücksetzungen sowie die sich ergebende Serialisierungsreihenfolge:

  1. (a)

    Zeitmarkenverfahren (Basic Timestamp Ordering),

  2. (b)

    BOCC,

  3. (c)

    FOCC,

  4. (d)

    RX-Sperrverfahren mit Wait/Die-Deadlock-Vermeidung,

  5. (e)

    RX-Sperrverfahren mit Wound/Wait-Deadlock-Vermeidung,

  6. (f)

    RX-Sperrverfahren mit Deadlock-Erkennung,

  7. (g)

    RX-Sperrverfahren mit Mehrversionensynchronisation und Deadlock-Erkennung.

Übung 12.3 (Deadlock-Erkennung)

Wenden Sie den Algorithmus von Obermarck zur Erkennung des folgenden Deadlocks an. Geben Sie jeden Zwischenschritt an. Es gelte \(\textit{ts}(T_{1})<\textit{ts}(T_{2})<\textit{ts}(T_{3})\)).

figure b