Optimistic Locking mit Versionsspalten implementieren
AI generated
SELECT
JOIN
SQL · Nebenlaeufigkeit · Konfliktbehandlung
Optimistic Locking mit Versionsspalten implementieren
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.

17 Min. Lesezeit Versionsspalte · UPDATE WHERE version · Retry-Logik PostgreSQL · MySQL · SQL Server

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.

11. FAQ: Optimistic Locking mit Versionsspalten implementieren

1Was ist Optimistic Locking?
Erkennt Schreibkonflikte erst beim Schreiben, ohne Sperre waehrend des Lesens. Versionsspalte macht Aenderungen erkennbar.
2Wie erkenne ich einen Konflikt?
Ueber affected rows nach dem UPDATE, null Zeilen bedeuten Konflikt.
3Warum nicht vom Benutzer setzbar?
Manipulierter Wert wuerde die gesamte Konfliktpruefung wirkungslos machen.
4Fehlgeschlagenes Update automatisch wiederholen?
Nur bei relativen Aenderungen, bei absoluten Werten riskant, weil fremde Aenderungen ueberschrieben werden koennten.
5Timestamp statt Integer als Version?
Fast gleichwertig, mit Audit-Nutzen, aber theoretisches Kollisionsrisiko bei geringer Praezision.
6Wann ist Pessimistic Locking besser?
Bei hoher Konfliktdichte, wo viele Retries mehr Overhead verursachen als direktes Sperren.
7Ersetzt es das Isolation Level?
Nein, ergaenzt es auf Anwendungsebene fuer Schreibkonflikte, schuetzt nicht automatisch vor Lesekonsistenz-Problemen.
8Obergrenze fuer Retries noetig?
Ja, sonst kann eine Anwendung bei hoher Konkurrenz theoretisch endlos wiederholen.
9Funktioniert es mit ORMs?
Ja, die meisten ORMs unterstuetzen Versionsspalten nativ mit spezifischer Konflikt-Exception.
10Affected-Rows-Pruefung vergessen?
Gesamte Schutzwirkung geht verloren, null betroffene Zeilen ist kein SQL-Fehler.