Konflikte erkennen, ohne die Datenbank vorab zu sperren
Optimistic Locking loest gleichzeitige Schreibzugriffe ohne Datenbank-Sperren, indem jede Zeile eine Versionsspalte traegt und ein UPDATE nur dann erfolgreich ist, wenn die Version beim Schreiben noch mit der beim Lesen gesehenen Version uebereinstimmt. Weicht die Anzahl betroffener Zeilen von eins ab, hat eine andere Transaktion zwischenzeitlich geschrieben, und die Anwendung muss den Konflikt gezielt behandeln.
Inhaltsverzeichnis
- 1. Was Optimistic Locking ist und wann es sinnvoll ist
- 2. Das Versionsspalten-Pattern: Schema-Design
- 3. Der Update mit WHERE version=x
- 4. Konflikterkennung: betroffene Zeilen pruefen
- 5. Konflikt-Handling im Anwendungscode
- 6. Alternative: Timestamp oder Hash statt Integer-Version
- 7. Optimistic vs. Pessimistic Locking im Vergleich
- 8. Grenzen von Optimistic Locking
- 9. Kombination mit Isolation Levels
- 10. Zusammenfassung
- 11. FAQ
1. Was Optimistic Locking ist und wann es sinnvoll ist
Optimistic Locking geht davon aus, dass Schreibkonflikte selten sind, und verzichtet deshalb auf Datenbank-Sperren waehrend des Lesens. Statt eine Zeile beim Lesen zu sperren, wie es Pessimistic Locking tut, liest die Anwendung die Daten frei, arbeitet damit, und prueft erst beim Schreiben, ob die Zeile seither von jemand anderem geaendert wurde. Diese Optimismus-Annahme ist in vielen Web-Anwendungen realistisch, weil die Zeit zwischen Lesen und Schreiben durch Benutzereingaben oft mehrere Sekunden betraegt und echte Konflikte selten gleichzeitig auftreten.
Der zentrale Vorteil von Optimistic Locking ist, dass keine Datenbank-Sperre ueber die Dauer einer Benutzerinteraktion gehalten werden muss. Ein Formular, das ein Benutzer minutenlang offen laesst, bevor er speichert, wuerde bei Pessimistic Locking eine Zeile ueber die gesamte Zeit sperren und andere Nutzer blockieren. Bei Optimistic Locking entsteht keine Sperre, bis der tatsaechliche Schreibvorgang stattfindet, was den Durchsatz in Systemen mit vielen gleichzeitigen Lesern und wenigen tatsaechlichen Konflikten deutlich erhoeht.
Die folgenden Abschnitte zeigen das konkrete Versionsspalten-Pattern, von der Schema-Aenderung ueber den entscheidenden UPDATE-Befehl bis zur Frage, wie Anwendungscode auf einen erkannten Konflikt reagieren sollte.
2. Das Versionsspalten-Pattern: Schema-Design
Das Versionsspalten-Pattern fuegt jeder relevanten Tabelle eine zusaetzliche Ganzzahlspalte hinzu, ueblicherweise version genannt, die bei jedem erfolgreichen UPDATE um eins erhoeht wird. Diese Spalte ist der zentrale Baustein von Optimistic Locking: Sie macht jede Zeilenversion eindeutig identifizierbar, ohne dass die Datenbank selbst irgendeine zusaetzliche Sperrlogik implementieren muss. Die gesamte Konfliktbehandlung findet in der WHERE-Bedingung des UPDATE-Befehls statt.
-- Schema-Design fuer Optimistic Locking mit Versionsspalte
CREATE TABLE products (
product_id INT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price NUMERIC(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1
);
-- Bestehende Tabelle nachtraeglich erweitern
ALTER TABLE products ADD COLUMN version INT NOT NULL DEFAULT 1;
-- Index nicht notwendig, version wird immer zusammen
-- mit dem Primary Key in der WHERE-Klausel verwendet
Wichtig beim Schema-Design ist, dass die version-Spalte niemals direkt vom Anwendungscode aus einem Formularfeld gesetzt werden darf, sondern ausschliesslich von der Datenbank oder einer kontrollierten Anwendungsschicht inkrementiert wird. Wuerde ein Benutzer die Versionsnummer manipulieren koennen, waere die gesamte Konfliktbehandlung wirkungslos, weil jede beliebige Version als gueltig vorgetaeuscht werden koennte.
3. Der Update mit WHERE version=x
Das Herzstueck von Optimistic Locking ist ein UPDATE-Befehl, der die zuletzt gelesene Versionsnummer in der WHERE-Klausel mitfuehrt und die neue Version im SET-Teil um eins erhoeht. Stimmt die Version zum Zeitpunkt des Schreibens noch mit der beim Lesen gesehenen ueberein, betrifft das UPDATE genau eine Zeile und der Schreibvorgang gilt als erfolgreich. Hat eine andere Transaktion die Zeile zwischenzeitlich geaendert und damit die Version erhoeht, betrifft das UPDATE null Zeilen, was der klare Indikator fuer einen Konflikt ist.
-- Schritt 1: Zeile lesen, aktuelle Version merken
SELECT product_id, name, price, stock, version
FROM products
WHERE product_id = 42;
-- Ergebnis: version = 7
-- Schritt 2: Aenderung vorbereiten (im Anwendungscode, nicht in SQL)
-- Neuer Preis: 29.99
-- Schritt 3: Update mit Versionspruefung
UPDATE products
SET price = 29.99,
version = version + 1
WHERE product_id = 42
AND version = 7;
-- Anwendungscode prueft die Anzahl betroffener Zeilen (affected rows):
-- 1 Zeile betroffen -> Update erfolgreich, kein Konflikt
-- 0 Zeilen betroffen -> Konflikt, jemand anderes hat zwischenzeitlich
-- geschrieben, version ist jetzt hoeher als 7
Entscheidend an diesem Muster ist, dass die gesamte Konfliktpruefung in einer einzigen atomaren SQL-Anweisung stattfindet. Es gibt kein separates SELECT zur Versionspruefung unmittelbar vor dem UPDATE, das selbst wieder anfaellig fuer eine Race Condition waere. Die Datenbank prueft Bedingung und Schreibvorgang in einem einzigen, atomaren Schritt, wodurch Optimistic Locking zuverlaessig auch bei hoher Nebenlaeufigkeit funktioniert.
4. Konflikterkennung: betroffene Zeilen pruefen
Die Konflikterkennung bei Optimistic Locking haengt vollstaendig davon ab, dass der Anwendungscode nach jedem UPDATE explizit die Anzahl betroffener Zeilen auswertet. Fast jeder Datenbanktreiber liefert diesen Wert zurueck, in JDBC als Rueckgabewert von executeUpdate(), in PDO als rowCount(), in den meisten modernen ORMs als Teil des Ergebnisobjekts. Wird dieser Wert ignoriert, verschwindet die gesamte Schutzwirkung von Optimistic Locking, weil ein fehlgeschlagenes UPDATE mit null betroffenen Zeilen stillschweigend als Erfolg behandelt wuerde.
Ein haeufiger Fehler ist, sich stattdessen auf den Exit-Code oder das Fehlen einer Exception zu verlassen: Ein UPDATE mit einer WHERE-Bedingung, die keine Zeile trifft, ist kein SQL-Fehler, sondern eine vollkommen gueltige Anweisung, die nur zufaellig null Zeilen aendert. Deshalb muss die Pruefung der betroffenen Zeilenzahl explizit als eigener Schritt im Anwendungscode erfolgen, direkt nach jedem versionsgeschuetzten UPDATE.
5. Konflikt-Handling im Anwendungscode
Wird ein Konflikt erkannt, weil das UPDATE null Zeilen betroffen hat, muss die Anwendung entscheiden, wie sie reagiert. Die drei ueblichen Strategien sind: dem Benutzer den Konflikt anzeigen und ihn die aktuellen Daten neu laden lassen, automatisch die aktuelle Version neu laden und die Aenderung erneut versuchen, oder eine feldweise Zusammenfuehrung der Aenderungen anbieten. Welche Strategie passt, haengt stark vom Anwendungsfall ab: Bei einem einfachen Produktpreis reicht meist ein klarer Hinweis, bei einem kollaborativen Dokument ist eine Merge-Strategie oft die bessere Wahl.
-- Pseudocode fuer Anwendungslogik mit Optimistic-Locking-Konflikt
-- (angelehnt an gaengige ORM- und Datenbank-Treiber-APIs)
-- function updateProductPrice(productId, newPrice, expectedVersion) {
-- const result = db.execute(
-- "UPDATE products SET price = ?, version = version + 1 " +
-- "WHERE product_id = ? AND version = ?",
-- [newPrice, productId, expectedVersion]
-- );
--
-- if (result.affectedRows === 0) {
-- // Konflikt erkannt: aktuelle Zeile neu laden
-- const current = db.query(
-- "SELECT * FROM products WHERE product_id = ?", [productId]
-- );
-- throw new OptimisticLockException(
-- "Product was modified by another user", current
-- );
-- }
--
-- return { success: true, newVersion: expectedVersion + 1 };
-- }
-- Retry-Strategie mit begrenzter Anzahl Versuche
-- for (let attempt = 0; attempt < 3; attempt++) {
-- try {
-- return updateProductPrice(productId, newPrice, currentVersion);
-- } catch (OptimisticLockException e) {
-- currentVersion = e.currentRow.version;
-- // Bei einfachen Feldern: Aenderung auf neue Basis anwenden
-- // Bei komplexen Aenderungen: Fehler an Benutzer weitergeben
-- }
-- }
Eine automatische Retry-Schleife ist nur dann sinnvoll, wenn die Aenderung selbst konfliktfrei mit der neuen Version kombinierbar ist, etwa bei einer relativen Aenderung wie einer Lagerbestandsreduzierung. Bei absoluten Wertaenderungen, etwa einem vom Benutzer eingetippten neuen Preis, ist ein automatischer Retry riskant, weil er die Aenderung eines anderen Benutzers stillschweigend ueberschreiben wuerde. In diesem Fall ist es korrekter, den Konflikt sichtbar zu machen und den Benutzer aktiv entscheiden zu lassen.
6. Alternative: Timestamp oder Hash statt Integer-Version
Statt eines einfachen Integer-Zaehlers kann die Versionsspalte auch als Zeitstempel implementiert werden, der bei jedem UPDATE auf den aktuellen Zeitpunkt gesetzt wird. Dieser Ansatz hat den Vorteil, dass er gleichzeitig auch als Audit-Information dient, wann eine Zeile zuletzt geaendert wurde, hat aber den Nachteil, dass bei sehr schneller Aktualisierungsfrequenz und geringer Timestamp-Praezision theoretisch zwei Aenderungen denselben Zeitstempel erhalten koennten.
Eine weitere Alternative ist ein Hash ueber die relevanten Spaltenwerte der Zeile, der bei jedem Lesen berechnet und beim Schreiben verglichen wird, statt eine dedizierte Versionsspalte zu pflegen. Dieser Ansatz erkennt Konflikte auch dann, wenn eine externe Aenderung ausserhalb der eigenen Anwendung stattfand und keine Versionsspalte inkrementiert hat, kostet aber mehr Rechenaufwand pro Lese- und Schreibvorgang. In der Praxis ist die klassische Integer-Versionsspalte die einfachste und am weitesten verbreitete Loesung, weil sie explizit, performant und leicht nachvollziehbar ist.
-- Timestamp-basierte Versionsspalte als Alternative
CREATE TABLE documents (
document_id INT PRIMARY KEY,
content TEXT NOT NULL,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Update mit Timestamp-Pruefung statt Integer-Version
UPDATE documents
SET content = 'Neuer Inhalt',
updated_at = CURRENT_TIMESTAMP
WHERE document_id = 17
AND updated_at = '2026-07-24 10:15:32.123456';
-- 0 betroffene Zeilen bedeuten auch hier einen Konflikt
-- Trigger, der updated_at automatisch pflegt (PostgreSQL)
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER documents_set_updated_at
BEFORE UPDATE ON documents
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
7. Optimistic vs. Pessimistic Locking im Vergleich
Die Wahl zwischen Optimistic Locking und Pessimistic Locking mit SELECT FOR UPDATE haengt vom erwarteten Konfliktaufkommen und vom Zeitraum zwischen Lesen und Schreiben ab. Die folgende Tabelle stellt beide Ansaetze gegenueber.
| Kriterium | Optimistic Locking | Pessimistic Locking |
|---|---|---|
| Sperrverhalten | Keine Sperre bis zum Schreiben | Sperre ab dem Lesen mit FOR UPDATE |
| Ideal bei | Seltenen Konflikten, langer Lese-Schreib-Zeit | Haeufigen Konflikten, kurzer Transaktionsdauer |
| Fehlerfall | Konflikt erst beim Schreiben sichtbar | Wartezeit oder sofortiger Timeout beim Lesen |
| Skalierung | Sehr gut bei vielen parallelen Lesern | Begrenzt durch Sperrenwarteschlangen |
| Implementierung | Versionsspalte plus Anwendungslogik | SELECT FOR UPDATE in der Datenbank |
8. Grenzen von Optimistic Locking
Optimistic Locking stoesst an seine Grenzen, wenn die Konfliktrate hoch ist. Bei vielen parallelen Schreibzugriffen auf dieselbe Zeile, etwa einem stark umkaempften Lagerbestand waehrend eines Sale-Events, fuehren wiederholte fehlgeschlagene Updates zu einer hohen Anzahl an Retries, was in Summe mehr Datenbank-Roundtrips verursacht als ein einziges Pessimistic-Locking-basiertes UPDATE. Ab einer bestimmten Konfliktdichte kehrt sich der Performance-Vorteil von Optimistic Locking damit um.
Ein weiteres Risiko ist eine unbegrenzte Retry-Schleife ohne Obergrenze: Bei sehr hoher Konkurrenz auf eine einzelne Zeile kann eine Anwendung theoretisch endlos wiederholen, ohne jemals erfolgreich zu schreiben. Eine feste maximale Anzahl an Versuchen mit anschliessendem Fehler an den Benutzer ist deshalb Pflicht, ebenso wie ein kurzer, randomisierter Backoff zwischen den Versuchen, um Thundering-Herd-Effekte bei gleichzeitigen Retries zu vermeiden.
9. Kombination mit Isolation Levels
Optimistic Locking ist eine Ergaenzung auf Anwendungsebene, kein Ersatz fuer ein angemessenes Isolation Level. Selbst mit korrektem Versionsspalten-Pattern schuetzt READ COMMITTED allein nicht vor Non-Repeatable Reads waehrend der Berechnung der neuen Werte, falls die Anwendung mehrere zusammenhaengende Lesevorgaenge durchfuehrt, bevor sie das versionsgeschuetzte UPDATE absetzt. In solchen Faellen ist REPEATABLE READ die sicherere Basis, kombiniert mit der Versionsspalte fuer die eigentliche Schreibkonflikt-Erkennung.
Der grosse Vorteil dieser Kombination ist, dass das Isolation Level fuer die Lesekonsistenz innerhalb der Transaktion sorgt, waehrend die Versionsspalte spezifisch den Schreibkonflikt zwischen zwei getrennten Transaktionen erkennt, die jeweils fuer sich isoliert korrekt ablaufen. Details zu den vier Isolation Levels und ihren jeweiligen Garantien behandelt der vertiefende Beitrag zu Isolation Levels von Read Committed bis Serializable.
-- Optimistic Locking kombiniert mit Repeatable Read
BEGIN;
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- Mehrere zusammenhaengende Lesevorgaenge, stabil dank Repeatable Read
SELECT stock, version FROM inventory WHERE product_id = 42;
SELECT reserved FROM reservations WHERE product_id = 42;
-- Berechnung im Anwendungscode auf Basis beider konsistenter Werte
-- Versionsgeschuetztes UPDATE als abschliessender Schreibvorgang
UPDATE inventory
SET stock = stock - 3,
version = version + 1
WHERE product_id = 42
AND version = 7;
COMMIT;
-- Repeatable Read sichert die Lesephase ab,
-- die Versionsspalte sichert die Schreibphase ab
10. Zusammenfassung
Optimistic Locking mit einer Versionsspalte erkennt Schreibkonflikte, ohne waehrend des Lesens eine Datenbank-Sperre zu halten. Das zentrale Pattern ist ein UPDATE mit WHERE version = erwarteteVersion, das die betroffene Zeilenzahl als Konfliktindikator nutzt: eine betroffene Zeile bedeutet Erfolg, null Zeilen bedeuten einen Konflikt, den der Anwendungscode explizit behandeln muss. Diese Pruefung muss immer als eigener Schritt nach jedem UPDATE stattfinden, niemals implizit vorausgesetzt werden.
Optimistic Locking eignet sich am besten fuer Szenarien mit seltenen Konflikten und laengeren Zeitraeumen zwischen Lesen und Schreiben, etwa Formulare mit Benutzereingaben. Bei sehr hoher Konfliktdichte ist Pessimistic Locking mit SELECT FOR UPDATE oft die bessere Wahl. Beide Ansaetze schliessen sich nicht aus, sondern koennen je nach Tabelle und Use-Case gezielt kombiniert werden, immer eingebettet in ein passend gewaehltes Isolation Level.
Optimistic Locking mit Versionsspalten, das Wichtigste auf einen Blick
Versionsspalte
Ganzzahlspalte, die bei jedem erfolgreichen UPDATE inkrementiert wird, niemals direkt vom Benutzer setzbar.
Update-Pattern
UPDATE ... SET version = version + 1 WHERE id = x AND version = erwartet, atomar in einer Anweisung.
Konflikterkennung
Affected-Rows-Zahl nach dem UPDATE pruefen, null bedeutet Konflikt, niemals implizit annehmen.
Grenzen
Bei hoher Konfliktdichte mehr Roundtrips als Pessimistic Locking, immer mit Retry-Obergrenze absichern.