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.
Inhaltsverzeichnis
- 1. Welches Problem Locking-Strategien lösen
- 2. Optimistic Locking: Versionsspalten als Grundprinzip
- 3. Optimistic Locking in PHP implementieren
- 4. Pessimistic Locking: SELECT FOR UPDATE im Detail
- 5. Pessimistic Locking in PHP mit PDO-Transaktionen
- 6. Deadlocks erkennen und systematisch vermeiden
- 7. Konfliktbehandlung: Retry-Strategien für Nutzer
- 8. Gemischte Strategien in realen Anwendungen
- 9. Optimistic vs. Pessimistic Locking im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.