Optimistic und Pessimistic Locking bei Datenbankzugriffen in PHP
AI generated
<?php
8.4
PHP · Datenbank · Nebenläufigkeit
Optimistic und Pessimistic Locking
Gleichzeitige Datenbankzugriffe kontrolliert absichern

Optimistic Locking und Pessimistic Locking sind zwei grundverschiedene Strategien, um zu verhindern, dass gleichzeitige Schreibzugriffe sich gegenseitig überschreiben. Wer versteht, wann eine Versionsspalte reicht und wann eine echte Datenbanksperre mit SELECT FOR UPDATE nötig ist, vermeidet sowohl stille Datenverluste als auch unnötige Performance-Einbußen durch übervorsichtiges Locking.

18 Min. Lesezeit Versionsspalte · SELECT FOR UPDATE · Deadlocks PHP 8.4 · PDO · MySQL/PostgreSQL

1. Welches Problem Locking-Strategien lösen

Sobald mehrere Prozesse gleichzeitig dieselbe Datenbankzeile lesen und ändern wollen, entsteht ein klassisches Nebenläufigkeitsproblem: Zwei Nutzer laden denselben Datensatz, beide nehmen Änderungen vor, und wer zuletzt speichert, überschreibt stillschweigend die Änderung des anderen. Ohne Locking-Strategie bemerkt niemand diesen Datenverlust, weil aus Sicht der Datenbank jede einzelne UPDATE-Anweisung erfolgreich war.

Optimistic Locking und Pessimistic Locking sind die beiden etablierten Antworten auf dieses Problem, mit fundamental unterschiedlicher Philosophie. Optimistic Locking geht davon aus, dass Konflikte selten sind, und prüft erst beim Speichern, ob zwischenzeitlich eine andere Änderung stattgefunden hat. Pessimistic Locking geht vom Gegenteil aus und blockiert eine Zeile bereits beim Lesen, sodass kein anderer Prozess sie in der Zwischenzeit ändern kann.

Die Wahl zwischen Optimistic Locking und Pessimistic Locking ist keine akademische Frage, sondern hat direkte Auswirkungen auf Durchsatz und Nutzererfahrung. Ein zu aggressives Pessimistic Locking reduziert Nebenläufigkeit unnötig und erzeugt Wartezeiten, während zu sorgloses Optimistic Locking in Systemen mit hoher Konfliktrate ständige Wiederholversuche erzwingt.

2. Optimistic Locking: Versionsspalten als Grundprinzip

Das Grundprinzip von Optimistic Locking ist eine zusätzliche Spalte, meist version genannt, die bei jedem UPDATE inkrementiert wird. Beim Laden eines Datensatzes merkt sich die Anwendung die aktuelle Versionsnummer. Beim Speichern wird das UPDATE mit einer WHERE-Bedingung ausgeführt, die sowohl die ID als auch die ursprünglich gelesene Version prüft. Hat sich die Version in der Zwischenzeit durch ein anderes UPDATE geändert, betrifft die eigene Anweisung null Zeilen, was die Anwendung als Konflikt erkennt.

Dieser Mechanismus benötigt keine dauerhaft gehaltene Datenbanksperre. Zwischen dem Lesen und dem Schreiben kann beliebig viel Zeit vergehen, etwa während ein Nutzer ein Formular ausfüllt, ohne dass andere Prozesse blockiert werden. Optimistic Locking eignet sich deshalb besonders gut für Webanwendungen, bei denen zwischen Anzeige und Speicherung eines Formulars mehrere Sekunden oder Minuten liegen können.

3. Optimistic Locking in PHP implementieren

Die konkrete Umsetzung von Optimistic Locking in PHP kombiniert eine version-Spalte in der Datenbank mit einer UPDATE-Anweisung, die auf genau diese Version prüft. Ein Domänenobjekt trägt seine geladene Version als Property, und die Repository-Methode zum Speichern gibt zurück, ob das UPDATE tatsächlich eine Zeile betroffen hat. Betrifft es keine Zeile, hat ein anderer Prozess den Datensatz zwischenzeitlich verändert, und die Anwendung muss den Konflikt behandeln, statt ihn stillschweigend zu ignorieren.

Wichtig bei Optimistic Locking ist, den Rückgabewert von rowCount() tatsächlich auszuwerten. Ein häufiger Fehler ist, das UPDATE auszuführen und den Erfolg anzunehmen, ohne zu prüfen, ob überhaupt eine Zeile geändert wurde. Genau diese Prüfung ist der Kern des gesamten Mechanismus.


<?php

declare(strict_types=1);

final class OptimisticLockException extends RuntimeException
{
}

final class ProductRepository
{
    public function __construct(private readonly PDO $pdo)
    {
    }

    public function findById(int $id): ?Product
    {
        $statement = $this->pdo->prepare(
            'SELECT id, name, stock, version FROM products WHERE id = ?'
        );
        $statement->execute([$id]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);

        if ($row === false) {
            return null;
        }

        return new Product((int) $row['id'], (string) $row['name'], (int) $row['stock'], (int) $row['version']);
    }

    /** @throws OptimisticLockException When another process changed the row concurrently. */
    public function save(Product $product): void
    {
        $statement = $this->pdo->prepare(
            'UPDATE products SET name = ?, stock = ?, version = version + 1
             WHERE id = ? AND version = ?'
        );
        $statement->execute([$product->name, $product->stock, $product->id, $product->version]);

        if ($statement->rowCount() === 0) {
            throw new OptimisticLockException(
                "Product {$product->id} was modified concurrently, reload and retry"
            );
        }
    }
}

final class Product
{
    public function __construct(
        public readonly int $id,
        public readonly string $name,
        public readonly int $stock,
        public readonly int $version,
    ) {
    }
}

4. Pessimistic Locking: SELECT FOR UPDATE im Detail

Pessimistic Locking verfolgt einen entgegengesetzten Ansatz: Statt Konflikte erst beim Speichern zu erkennen, wird eine Zeile bereits beim Lesen für andere Transaktionen gesperrt. In MySQL und PostgreSQL geschieht das über SELECT ... FOR UPDATE innerhalb einer expliziten Transaktion. Jede andere Transaktion, die versucht, dieselbe Zeile ebenfalls mit FOR UPDATE zu lesen oder zu ändern, wartet, bis die haltende Transaktion committet oder zurückgerollt wird.

Diese Sperre ist besonders wertvoll bei Vorgängen, die zwingend konsistent sein müssen und bei denen ein Konflikt nicht einfach durch einen erneuten Versuch gelöst werden kann, etwa beim Reservieren von Lagerbestand oder beim Verarbeiten von Zahlungen. Pessimistic Locking verhindert hier von vornherein, dass zwei Prozesse gleichzeitig denselben letzten verfügbaren Lagerbestand verkaufen.

5. Pessimistic Locking in PHP mit PDO-Transaktionen

In PHP setzt man Pessimistic Locking um, indem man eine Transaktion startet, die Zeile mit SELECT ... FOR UPDATE lädt, die Geschäftslogik ausführt und die Transaktion anschließend committet. Solange die Transaktion offen ist, hält die Datenbank die Sperre auf der betroffenen Zeile. Wichtig ist, diese Transaktion so kurz wie möglich zu halten, weil jede zusätzliche Millisekunde innerhalb der Sperre die Wartezeit für konkurrierende Prozesse verlängert.

Ein häufiger Fehler bei Pessimistic Locking ist, innerhalb der offenen Transaktion zusätzliche, langsame Operationen wie externe HTTP-Aufrufe oder E-Mail-Versand auszuführen. Solche Operationen gehören strikt außerhalb der Transaktion, direkt nach dem Commit, damit die Datenbanksperre nicht länger als nötig gehalten wird.


<?php

declare(strict_types=1);

final class InventoryService
{
    public function __construct(private readonly PDO $pdo)
    {
    }

    /** @throws RuntimeException When stock is insufficient. */
    public function reserveStock(int $productId, int $quantity): void
    {
        $this->pdo->beginTransaction();

        try {
            // Row is locked for the duration of this transaction
            $statement = $this->pdo->prepare(
                'SELECT stock FROM products WHERE id = ? FOR UPDATE'
            );
            $statement->execute([$productId]);
            $currentStock = (int) $statement->fetchColumn();

            if ($currentStock < $quantity) {
                throw new RuntimeException("Insufficient stock for product {$productId}");
            }

            $update = $this->pdo->prepare(
                'UPDATE products SET stock = stock - ? WHERE id = ?'
            );
            $update->execute([$quantity, $productId]);

            $this->pdo->commit();
        } catch (Throwable $e) {
            $this->pdo->rollBack();
            throw $e;
        }

        // Slow operations happen AFTER commit, outside the lock
        // $this->notifier->sendStockAlert($productId);
    }
}

6. Deadlocks erkennen und systematisch vermeiden

Ein Deadlock entsteht, wenn zwei Transaktionen versuchen, dieselben Zeilen in unterschiedlicher Reihenfolge zu sperren: Transaktion A hält eine Sperre auf Zeile 1 und wartet auf Zeile 2, während Transaktion B Zeile 2 hält und auf Zeile 1 wartet. Beide Transaktionen warten unbegrenzt aufeinander, bis die Datenbank den Deadlock erkennt und eine der beiden Transaktionen mit einem Fehler abbricht. Pessimistic Locking erhöht das Risiko solcher Deadlocks, weil es aktiv Sperren hält, während Optimistic Locking dieses Risiko strukturell vermeidet.

Die wirksamste Gegenmaßnahme gegen Deadlocks bei Pessimistic Locking ist eine konsistente Sperrreihenfolge: Wenn eine Transaktion mehrere Zeilen sperren muss, etwa bei einem Transfer zwischen zwei Konten, sollten alle Transaktionen im System diese Zeilen immer in derselben Reihenfolge sperren, zum Beispiel sortiert nach Primärschlüssel. Zusätzlich sollte PHP-Code auf einen Deadlock-Fehler von der Datenbank mit einem automatischen, begrenzten Retry reagieren, da Deadlocks in der Praxis nie vollständig vermeidbar sind.

7. Konfliktbehandlung: Retry-Strategien für Nutzer

Bei Optimistic Locking ist ein erkannter Konflikt kein Fehlerfall im klassischen Sinn, sondern ein normaler, zu erwartender Ablaufzweig. Die Anwendung sollte den betroffenen Datensatz neu laden, die Änderungen des Nutzers auf Konflikte prüfen und entweder automatisch zusammenführen oder dem Nutzer die widersprüchlichen Werte zur manuellen Entscheidung vorlegen. Ein stilles, automatisches Überschreiben nach einem erkannten Konflikt würde den gesamten Zweck von Optimistic Locking zunichtemachen.

Für technische Konflikte, etwa Deadlocks bei Pessimistic Locking, eignet sich hingegen ein automatischer Retry mit kurzer, zufälliger Wartezeit zwischen den Versuchen, um zu verhindern, dass mehrere wiederholende Prozesse erneut gegeneinander laufen. Eine feste Obergrenze an Wiederholversuchen verhindert, dass ein dauerhaft blockierter Prozess die Anwendung unbegrenzt belastet.

8. Gemischte Strategien in realen Anwendungen

Reale Anwendungen setzen selten ausschließlich eine der beiden Strategien ein. Ein typisches Muster ist Optimistic Locking für die meisten Formulare und Bearbeitungsvorgänge, kombiniert mit gezieltem Pessimistic Locking für einzelne, besonders konfliktträchtige Operationen wie Lagerbestandsreservierung oder Zahlungsabwicklung. Diese Kombination nutzt die Vorteile beider Ansätze, ohne die Nachteile des jeweils anderen flächendeckend in Kauf zu nehmen.

Eine weitere Variante ist, Optimistic Locking als Standardfall zu implementieren und erst bei wiederholt beobachteten Konflikten in Produktionsdaten gezielt auf Pessimistic Locking für die betroffene Operation umzustellen. Dieser datengetriebene Ansatz verhindert vorschnelle Pessimierung von Bereichen, die in der Praxis kaum Konflikte erzeugen.

9. Optimistic vs. Pessimistic Locking im Vergleich

Die Entscheidung zwischen Optimistic Locking und Pessimistic Locking hängt von der erwarteten Konfliktrate, der Kritikalität der Operation und der akzeptablen Wartezeit ab.

Kriterium Optimistic Locking Pessimistic Locking
Passend bei Seltenen Konflikten Häufigen, kritischen Konflikten
Sperrdauer Keine Sperre nötig Sperre während der Transaktion
Nebenläufigkeit Hoch Reduziert durch Wartezeit
Deadlock-Risiko Strukturell ausgeschlossen Vorhanden, muss behandelt werden
Umgang mit Konflikten Nach dem Auftreten, per Retry/Merge Von vornherein verhindert

Optimistic Locking ist der bessere Standardfall für die meisten Anwendungen, weil es Nebenläufigkeit maximiert und keine dauerhaften Sperren benötigt. Pessimistic Locking bleibt jedoch unverzichtbar für Operationen, bei denen ein nachträglich erkannter Konflikt inakzeptabel ist, etwa beim Verhindern von doppelten Buchungen auf ein Konto mit begrenztem Guthaben.

10. Zusammenfassung

Optimistic Locking und Pessimistic Locking lösen dasselbe Grundproblem, gleichzeitige Datenbankzugriffe ohne stillen Datenverlust, mit unterschiedlichen Mitteln. Eine Versionsspalte mit geprüftem UPDATE reicht für die meisten Formulare und Bearbeitungsvorgänge aus, während SELECT ... FOR UPDATE innerhalb kurzer, gezielter Transaktionen für kritische, konfliktträchtige Operationen wie Lagerbestandsreservierung notwendig bleibt.

Wichtig ist in beiden Fällen ein durchdachter Umgang mit dem Konfliktfall: Bei Optimistic Locking muss der Rückgabewert des UPDATE tatsächlich geprüft werden, bei Pessimistic Locking muss die Sperrzeit minimiert und ein Retry-Mechanismus gegen Deadlocks vorgesehen werden. Wer beide Strategien versteht, kann sie gezielt kombinieren, statt eine einzelne Lösung pauschal auf die gesamte Anwendung anzuwenden.

Optimistic und Pessimistic Locking — Das Wichtigste auf einen Blick

Optimistic Locking

Versionsspalte plus geprüftes UPDATE, keine dauerhafte Sperre nötig.

Pessimistic Locking

SELECT ... FOR UPDATE in kurzer Transaktion, verhindert Konflikte von vornherein.

Deadlock-Vermeidung

Konsistente Sperrreihenfolge und automatischer, begrenzter Retry.

Kombination

Optimistic als Standard, Pessimistic gezielt für kritische Operationen.

11. FAQ: Optimistic und Pessimistic Locking

1Was ist der Unterschied zwischen Optimistic und Pessimistic Locking?
Optimistic prüft erst beim Speichern, Pessimistic sperrt bereits beim Lesen.
2Wie funktioniert Optimistic Locking?
Über eine Versionsspalte, die beim Speichern per WHERE-Bedingung geprüft wird.
3Wann Pessimistic statt Optimistic?
Bei kritischen Operationen wie Lagerbestand oder Zahlungen, wo Konflikte von vornherein verhindert werden müssen.
4Wie setzt man SELECT FOR UPDATE um?
In einer kurzen, expliziten PDO-Transaktion, langsame Operationen nach dem Commit.
5Was ist ein Deadlock?
Zwei Transaktionen sperren dieselben Zeilen in umgekehrter Reihenfolge und warten unbegrenzt aufeinander.
6Wie vermeidet man Deadlocks?
Konsistente Sperrreihenfolge und automatischer, begrenzter Retry-Mechanismus.
7Was passiert bei erkanntem Konflikt?
Neu laden und dem Nutzer die widersprüchlichen Werte zur Entscheidung vorlegen.
8Können beide kombiniert werden?
Ja, üblich ist Optimistic als Standard und Pessimistic gezielt für kritische Operationen.
9Reduziert Pessimistic Locking die Nebenläufigkeit?
Ja, konkurrierende Prozesse warten auf die Sperre, daher die Transaktion so kurz wie möglich halten.
10Ist Optimistic immer die sicherere Wahl?
Nicht immer, bei sehr hoher Konfliktrate ist Pessimistic Locking oft robuster.