Optimistic Locking
Konflikte erst beim Speichern erkennen
Die Idee und ihr Nutzen
Frau Berg und Herr Kurz arbeiten in der Schadenabteilung einer Versicherung und öffnen fast gleichzeitig denselben Schaden 4711. Frau Berg erhöht die Reserve und speichert. Die Reserve ist der Betrag, den die Versicherung für diesen Schaden zurücklegt. Herr Kurz sieht auf seinem Bildschirm noch den alten Stand und ändert die Adresse. Beim Speichern würde Herr Kurz die neue Reserve unbemerkt mit dem alten Wert überschreiben. Deshalb trägt jeder Datensatz eine Versionsnummer, die bei jeder Änderung um eins steigt. Die Datenbank nimmt eine Änderung nur an, wenn die mitgeschickte Versionsnummer noch zur gespeicherten passt. Herr Kurz hat den Schaden mit Version 7 geöffnet, nach dem Speichern von Frau Berg steht aber Version 8 in der Datenbank. Den Speicherversuch von Herrn Kurz lehnt die Datenbank deshalb ab. Dieses Verfahren heißt Optimistic Locking, auf Deutsch optimistisches Sperren. Optimistisch heißt das Verfahren, weil es für Fälle gedacht ist, in denen Konflikte selten sind. Es sperrt den Schaden während der Bearbeitung nicht, und die Datenbank prüft erst beim Speichern, ob inzwischen jemand anderes gespeichert hat.
Wie funktioniert es?
Frau Berg öffnet Schaden 4711 in der Schadenanwendung. Die Schadenanwendung lädt den Schaden mit Version 7 aus der Datenbank und merkt sich diese Nummer. Gleich danach öffnet Herr Kurz denselben Schaden und bekommt ebenfalls Version 7. Die Reserve beträgt zu diesem Zeitpunkt 5.000 Euro. Frau Berg ändert die Reserve auf 8.000 Euro und speichert zuerst. Die Schadenanwendung schickt dabei die Bedingung „nur wenn Version 7“ mit. Die Datenbank findet Version 7, übernimmt die Änderung und speichert den Schaden als Version 8. Eine Minute später speichert Herr Kurz seine Änderung an der Adresse des Geschädigten, und die Schadenanwendung schickt wieder die Bedingung „nur wenn Version 7“ mit. In der Datenbank steht inzwischen Version 8. Die Bedingung trifft deshalb auf keinen Datensatz zu, und die Datenbank meldet „0 Zeilen geändert (Version ist 8)“. Daran erkennt die Schadenanwendung den Konflikt. Herr Kurz sieht die Meldung „Der Schaden wurde inzwischen von Frau Berg geändert. Bitte prüfen Sie Ihre Änderung.“ Die Schadenanwendung zeigt ihm den neuen Stand mit der Reserve von 8.000 Euro und hebt seine Adressänderung hervor. Herr Kurz übernimmt seine Adressänderung in den neuen Stand und speichert erneut, diesmal mit Version 8. Die Datenbank findet Version 8, übernimmt die Änderung und speichert Version 9. Version 9 enthält die neue Reserve von Frau Berg und die neue Adresse von Herrn Kurz.
Die Versionsnummer steht in einem zusätzlichen Feld jedes Datensatzes. Der Befehl, mit dem die Schadenanwendung speichert, lautet sinngemäß: „Ändere Schaden 4711 und setze die Version auf 8, aber nur, wenn die gespeicherte Version noch 7 ist.“ Prüfung und Änderung geschehen in einem einzigen Befehl, damit niemand zwischen Prüfen und Schreiben eine andere Änderung speichern kann. Das Verfahren erkennt den Konflikt nur. Welche Werte im neuen Stand gelten, entscheidet die Person, deren Speicherversuch abgewiesen wurde, bei Schaden 4711 also Herr Kurz.
Statt einer Versionsnummer kann auch ein Zeitstempel der letzten Änderung dienen, wenn er genau genug ist, um zwei Änderungen sicher zu unterscheiden. Bei Webschnittstellen, über die Programme nach dem Standard HTTP Daten abrufen und ändern, gibt es dafür den ETag. Der ETag ist eine Kennung für den gelesenen Stand, etwa „v7“, die der Server mitschickt und die der Aufrufer beim Ändern als Bedingung zurückschickt. Verlorene Änderungen verhindert der ETag nur, wenn der Server diese Bedingung beim Ändern tatsächlich prüft und eine Änderung mit veraltetem ETag ablehnt.
Optimistisches Sperren: die Versionsnummer weist die veraltete Änderung ab
Wann ist es sinnvoll und wann nicht?
Optimistic Locking lohnt sich
- Ohne Versionsnummer gehen Änderungen unbemerkt verloren. Die Schadenanwendung schreibt dann den gesamten Datensatz so zurück, wie er auf dem Bildschirm von Herrn Kurz steht, also mit der neuen Adresse und der alten Reserve von 5.000 Euro. Keiner der beiden sieht eine Warnung. Solche verlorenen Änderungen fallen oft erst Wochen später auf, zum Beispiel wenn die Reserve nicht zur Schadenhöhe passt. Mit der Versionsnummer erfährt Herr Kurz sofort beim Speichern vom Konflikt.
- Viele Personen oder Programme arbeiten gleichzeitig mit derselben Datenbank, ändern denselben Datensatz aber nur selten in denselben Minuten. Würde die Schadenanwendung jeden geöffneten Schaden sperren, müsste Herr Kurz warten, solange Frau Berg denselben Schaden geöffnet hat, obwohl ein Konflikt selten ist. Mit Optimistic Locking muss niemand beim Öffnen eines Schadens auf eine Sperre warten.
- Zwischen Lesen und Speichern vergehen Minuten, wie in Bildschirmanwendungen und Webanwendungen üblich, und eine abgewiesene Änderung ist schnell neu eingetragen, wie die Adresse von Herrn Kurz. Eine Sperre in der Datenbank müsste dafür minutenlang eine Verbindung zur Datenbank belegen. Eine Sperre, die die Schadenanwendung als eigenen Eintrag speichert, hält Herrn Kurz so lange auf und braucht eine Ablaufzeit, damit sie nach einem geschlossenen Browser nicht bestehen bleibt. Optimistic Locking braucht keine dieser Sperren.
- Ein System besteht aus mehreren Programmen, die dieselben Datensätze ändern. Eine Sperre müssten alle diese Programme gemeinsam setzen und beachten; die Versionsnummer steht dagegen im Datensatz selbst, und jedes Programm liest sie mit dem Datensatz und schickt sie beim Speichern mit.
Optimistic Locking ist nicht das Richtige
- Konflikte sind häufig. Ein Beispiel sind mehrere Sachbearbeiter, die nach einem Sturm gleichzeitig denselben Großschaden bearbeiten. Mit Versionsnummern scheitern dann viele Speicherversuche, und Herr Kurz und die anderen Sachbearbeiter müssen ihre Änderungen immer wieder in einen neuen Stand übernehmen. Stattdessen sperrt die Schadenanwendung den Schaden mit Pessimistic Locking, sobald Frau Berg ihn zum Bearbeiten öffnet. Herr Kurz wartet, bis Frau Berg fertig ist und die Schadenanwendung die Sperre aufhebt. So scheitert kein Speicherversuch, und niemand muss eine Änderung erneut eingeben.
- Ein Programm ändert nur einen Zähler, etwa die Zahl der heute gemeldeten Schäden. Mit Versionsnummer müsste das Programm den Zähler lesen, den neuen Wert berechnen und ihn mit der Versionsnummer zurückschreiben. Bei vielen gleichzeitigen Meldungen würden viele dieser Speicherversuche abgewiesen. Stattdessen schickt das Programm der Datenbank einen Befehl wie „erhöhe den Zähler um 1“, und die Datenbank rechnet mit dem gerade gespeicherten Wert. Die Datenbank weist dabei keinen Befehl ab, und das Programm muss vorher nichts lesen und keine Versionsnummer mitschicken.
- Eine Bearbeitung dauert sehr lange. Ein Beispiel ist eine ausführliche Stellungnahme zu einem Schaden, an der Frau Berg eine Stunde schreibt. Wird ihr Speicherversuch abgewiesen, muss Frau Berg die ganze Stellungnahme mit dem neuen Stand abgleichen, nicht nur eine Adresse wie Herr Kurz. Stattdessen sperrt die Schadenanwendung den Schaden mit Pessimistic Locking, sobald Frau Berg mit der Stellungnahme beginnt. Herr Kurz kann den Schaden in dieser Stunde nicht ändern, aber die Stellungnahme von Frau Berg wird beim Speichern nicht abgewiesen.
Nachteile und typische Fehler
Die Person, deren Speicherversuch abgewiesen wird, erfährt erst beim Speichern vom Konflikt und muss ihre Änderung gegebenenfalls erneut eingeben. Die Schadenanwendung muss den Konflikt verständlich darstellen und das Zusammenführen erleichtern, indem sie den neuen Stand zeigt und die abgewiesene Änderung hervorhebt. Das ist zusätzlicher Aufwand in der Oberfläche. Jede Tabelle mit geschützten Datensätzen braucht das Versionsfeld, und jedes Programm, das diese Datensätze ändert, muss die Versionsnummer mitschicken. Das betrifft auch Programme, die die Datensätze ohne die Schadenanwendung ändern.
Typische Fehler und woran man sie erkennt:
- Programm ohne Versionsprüfung: Ein kleines Programm für Massenänderungen, ein sogenanntes Skript, liest morgens viele Schäden, darunter Schaden 4711. Später schreibt das Skript die Schäden ohne die Bedingung „nur wenn Version …“ zurück. Hat Frau Berg die Reserve in der Zwischenzeit geändert, überschreibt das Skript ihre Änderung mit dem alten Wert. Erkennbar ist das an Werten, die nach einem Skriptlauf auf einen alten Stand zurückfallen, ohne dass die Versionsnummer gestiegen ist.
- Bloße Fehlermeldung: Die Schadenanwendung meldet den Konflikt ohne neuen Stand und ohne Hinweis, was sich geändert hat. Herr Kurz muss den Schaden dann neu öffnen und die Adresse noch einmal eingeben, ohne zu wissen, was Frau Berg geändert hat. Erkennbar ist das an verärgerten Nutzern und an wiederholten Speicherversuchen, bei denen Änderungen von Hand übertragen werden.
- Prüfen und Schreiben getrennt: Die Schadenanwendung fragt für Herrn Kurz zuerst in einem eigenen Befehl die gespeicherte Versionsnummer ab und findet Version 7. Danach schreibt die Schadenanwendung die Adressänderung mit einem zweiten Befehl ohne Bedingung. Speichert Frau Berg ihre Reserve genau zwischen diesen beiden Befehlen, überschreibt die Änderung von Herrn Kurz die neue Reserve. Erkennbar ist das an verlorenen Änderungen trotz Versionsfeld, vor allem wenn viele Personen gleichzeitig speichern.
- Automatisch überschrieben: Die Schadenanwendung wiederholt den abgewiesenen Speicherversuch selbsttätig mit der neuen Versionsnummer, aber mit den alten Werten vom Bildschirm von Herrn Kurz, also auch mit der alten Reserve von 5.000 Euro. Erkennbar ist das daran, dass nie eine Konfliktmeldung erscheint und die Reserve trotzdem auf 5.000 Euro zurückfällt.
- Ungenauer Zeitstempel: Statt einer Versionsnummer dient ein Zeitstempel, der nur auf die Sekunde genau ist. Zwei Änderungen am selben Datensatz in derselben Sekunde tragen dann denselben Zeitstempel. Wer den Stand nach der ersten Änderung gelesen hat, überschreibt beim Speichern die zweite Änderung, ohne dass die Datenbank einen Konflikt meldet. Erkennbar ist das an aufeinanderfolgenden Änderungen mit gleichem Zeitstempel.
Weiterführend
Verwandte Ansätze
- Unit of Work: sammelt mehrere Änderungen und prüft beim gemeinsamen Speichern die Versionsnummern aller geänderten Datensätze.
- Distributed Lock: eine Sperre, die mehrere Programme auf verschiedenen Rechnern gemeinsam beachten.
- Idempotency: verhindert, dass eine wiederholte Anfrage doppelt wirkt. Das ist ein anderes Problem als verlorene Änderungen, und beide Verfahren ergänzen sich.
Werkzeuge und Umsetzungen
- Hibernate ORM: Java-Bibliothek für Datenbankzugriffe. Ein Feld mit der Kennzeichnung
@Versiongenügt, damit Hibernate die Versionsnummer bei jedem Speichern prüft und erhöht. - Entity Framework Core: Bibliothek für Datenbankzugriffe auf der Plattform .NET. Sie kennt ein Feld, das bei jedem Speichern auf Veränderung geprüft wird.
- PostgreSQL: Datenbank, bei der ein Änderungsbefehl mit Bedingung eine eigene Versionsspalte prüfen und beim erfolgreichen Schreiben erhöhen kann.
- Spring Data: Java-Bibliothek, die Versionsfelder für viele Datenbanken einheitlich unterstützt.
Quellen
- David Rice (Martin Fowler, Patterns of Enterprise Application Architecture): Optimistic Offline Lock: Katalogeintrag des Musters, das Konflikte zwischen gleichzeitigen Bearbeitungen erkennt und die spätere Änderung zurückweist.
- Microsoft: Handling Concurrency Conflicts (Entity Framework Core): offizielle Dokumentation; zeigt den Änderungsbefehl mit Versionsbedingung, die Erkennung über null geänderte Zeilen und das Auflösen durch Neuladen und erneutes Anwenden.