Nebenläufigkeit sicher steuern, ohne das System zu blockieren
Wenn mehrere Nutzer gleichzeitig denselben Datensatz ändern, entscheidet die Wahl zwischen Optimistic Locking und Pessimistic Locking darüber, ob Ihre Anwendung Lost Updates zuverlässig verhindert oder unter Last in Wartezeiten stecken bleibt. Dieser Artikel erklärt beide Sperrstrategien in MySQL mit echtem SQL- und PHP-Code, zeigt SELECT FOR UPDATE und Versionsspalten im Detail und hilft bei der Entscheidung, welches Modell zu welchem Anwendungsfall passt.
Inhaltsverzeichnis
- 1. Warum Nebenläufigkeit in Datenbanken zum Problem wird
- 2. Pessimistic Locking mit SELECT FOR UPDATE
- 3. Optimistic Locking mit einer Versionsspalte
- 4. Optimistic Locking in PHP mit PDO implementieren
- 5. Deadlocks bei Pessimistic Locking erkennen und vermeiden
- 6. Retry-Strategien für Optimistic-Locking-Konflikte
- 7. Entscheidungskriterien: Welches Modell für welchen Fall
- 8. Praxisbeispiel: Lagerbestand in einer E-Commerce-Anwendung
- 9. Optimistic Locking und Pessimistic Locking im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Nebenläufigkeit in Datenbanken zum Problem wird
Sobald zwei Transaktionen gleichzeitig denselben Datensatz lesen, verändern und zurückschreiben, entsteht ein klassisches Race-Condition-Problem: das Lost Update. Transaktion A liest einen Kontostand von 100, Transaktion B liest denselben Wert kurz danach ebenfalls, beide berechnen unabhängig voneinander einen neuen Wert und schreiben ihn zurück. Die zuletzt schreibende Transaktion überschreibt die Änderung der ersten, ohne dass ein Fehler auftritt. Genau dieses Szenario verhindern Optimistic Locking und Pessimistic Locking, allerdings mit grundverschiedenen Mechanismen.
Pessimistic Locking blockiert den Datensatz bereits beim Lesen, sodass keine zweite Transaktion parallel schreibend zugreifen kann. Optimistic Locking hingegen erlaubt parallelen Lesezugriff und prüft erst beim Schreiben, ob der Datensatz zwischenzeitlich verändert wurde. Beide Ansätze lösen das Lost-Update-Problem korrekt, unterscheiden sich aber massiv in Durchsatz, Wartezeiten und Implementierungsaufwand. Die folgenden Abschnitte zeigen beide Sperrstrategien mit konkretem SQL- und PHP-Code und liefern klare Entscheidungskriterien für den jeweiligen Anwendungsfall.
2. Pessimistic Locking mit SELECT FOR UPDATE
Pessimistic Locking geht davon aus, dass ein Konflikt wahrscheinlich ist, und verhindert ihn proaktiv durch eine Sperre. In MySQL mit InnoDB erreicht man das mit SELECT ... FOR UPDATE innerhalb einer expliziten Transaktion. Der Befehl setzt eine exklusive Zeilensperre auf die gelesenen Datensätze, die bis zum COMMIT oder ROLLBACK bestehen bleibt. Jede weitere Transaktion, die versucht, dieselbe Zeile mit FOR UPDATE zu lesen oder direkt zu schreiben, wird blockiert, bis die erste Transaktion die Sperre freigibt.
Diese Form des Pessimistic Locking eignet sich besonders für Szenarien mit hoher Konfliktwahrscheinlichkeit, etwa beim Reservieren von begrenztem Lagerbestand oder beim Verbuchen von Zahlungen auf ein Konto. Der Vorteil liegt in der Einfachheit: Es gibt keine Race Condition, weil konkurrierende Zugriffe seriell abgearbeitet werden. Der Nachteil ist die Wartezeit, die sich unter hoher Last summiert, sowie das Risiko von Deadlocks, wenn mehrere Transaktionen Sperren in unterschiedlicher Reihenfolge anfordern.
-- Pessimistic Locking: exclusive row lock until COMMIT
START TRANSACTION;
SELECT id, quantity, reserved
FROM inventory
WHERE product_id = 4711
FOR UPDATE;
-- Row is locked for all other transactions now
-- Application logic checks availability
UPDATE inventory
SET reserved = reserved + 1
WHERE product_id = 4711
AND (quantity - reserved) >= 1;
COMMIT;
-- Lock is released, waiting transactions can proceed
3. Optimistic Locking mit einer Versionsspalte
Optimistic Locking verzichtet komplett auf Datenbanksperren während der Lesephase. Stattdessen erhält jede Tabelle eine zusätzliche Spalte, meist version genannt, die bei jedem erfolgreichen Update um eins erhöht wird. Beim Schreiben prüft das UPDATE-Statement in der WHERE-Klausel, ob die Version noch mit dem beim Lesen erfassten Wert übereinstimmt. Stimmt sie nicht überein, hat eine andere Transaktion den Datensatz zwischenzeitlich verändert, und das Update betrifft null Zeilen, was die Anwendung als Konflikt erkennt.
Der zentrale Vorteil von Optimistic Locking ist der fehlende Wartezustand: Lesevorgänge blockieren nichts, und Schreibkonflikte werden erst im Moment des Commits erkannt, nicht vorab durch eine Sperre. Das erhöht den Durchsatz bei Anwendungen mit geringer Konfliktwahrscheinlichkeit erheblich, etwa bei der Bearbeitung von Nutzerprofilen oder Formularen, bei denen selten zwei Personen gleichzeitig denselben Datensatz bearbeiten. Der Nachteil: Die Anwendung muss den Konfliktfall explizit behandeln, meist durch eine Fehlermeldung oder einen Retry-Mechanismus.
-- Optimistic Locking: version column detects concurrent writes
CREATE TABLE product (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
version INT NOT NULL DEFAULT 1
) ENGINE=InnoDB;
-- Step 1: read current state including version
SELECT id, name, price, version FROM product WHERE id = 42;
-- Application caches version = 5
-- Step 2: write with version check in WHERE clause
UPDATE product
SET price = 29.90, version = version + 1
WHERE id = 42 AND version = 5;
-- affected rows = 0 means a concurrent write happened
4. Optimistic Locking in PHP mit PDO implementieren
Die Implementierung von Optimistic Locking in der Anwendungsschicht folgt einem festen Muster: Datensatz inklusive Version laden, Änderungen im PHP-Objekt vornehmen, dann ein bedingtes Update ausführen und die betroffene Zeilenzahl prüfen. PDO liefert diese Information über PDOStatement::rowCount(). Ist der Wert null, obwohl die WHERE-Bedingung auf die richtige ID zutrifft, war die Version nicht mehr aktuell, und die Anwendung muss reagieren, statt den Fehler stillschweigend zu ignorieren.
Wichtig ist, dass die Versionsprüfung und das Update in einem einzigen atomaren SQL-Statement stattfinden, nicht als separates SELECT gefolgt von einem ungeprüften UPDATE. Andernfalls entsteht zwischen Prüfung und Schreiben erneut ein Zeitfenster für eine Race Condition, wodurch das gesamte Optimistic Locking wirkungslos würde. Die folgende Klasse zeigt eine saubere Implementierung mit expliziter Konfliktbehandlung.
<?php
declare(strict_types=1);
final class OptimisticLockException extends RuntimeException
{
}
final class ProductRepository
{
public function __construct(private readonly PDO $pdo)
{
}
/**
* Loads a product together with its current version number.
*/
public function find(int $id): array
{
$stmt = $this->pdo->prepare(
'SELECT id, name, price, version FROM product WHERE id = :id'
);
$stmt->execute(['id' => $id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row === false) {
throw new RuntimeException("Product {$id} not found");
}
return $row;
}
/**
* Updates the price using optimistic locking.
* Throws OptimisticLockException on a version conflict.
*/
public function updatePrice(int $id, float $price, int $expectedVersion): void
{
$stmt = $this->pdo->prepare(
'UPDATE product
SET price = :price, version = version + 1
WHERE id = :id AND version = :version'
);
$stmt->execute([
'price' => $price,
'id' => $id,
'version' => $expectedVersion,
]);
if ($stmt->rowCount() === 0) {
throw new OptimisticLockException(
"Product {$id} was modified concurrently, version {$expectedVersion} is stale"
);
}
}
}
5. Deadlocks bei Pessimistic Locking erkennen und vermeiden
Ein Deadlock entsteht bei Pessimistic Locking, wenn zwei Transaktionen wechselseitig auf Sperren warten, die die jeweils andere Transaktion hält. Transaktion A sperrt Zeile 1 und wartet auf Zeile 2, während Transaktion B Zeile 2 sperrt und auf Zeile 1 wartet. InnoDB erkennt diese Situation automatisch über einen Wait-for-Graph und bricht eine der beiden Transaktionen mit dem Fehler ERROR 1213: Deadlock found when trying to get lock ab. Die Anwendung muss diesen Fehler abfangen und die betroffene Transaktion erneut ausführen.
Der wirksamste Schutz vor Deadlocks ist eine konsistente Sperrreihenfolge: Wenn alle Transaktionen Zeilen immer in derselben Reihenfolge sperren, etwa sortiert nach Primärschlüssel, kann kein Zyklus im Wait-for-Graph entstehen. Zusätzlich reduziert eine kurze Transaktionsdauer die Wahrscheinlichkeit von Konflikten insgesamt, weil das Zeitfenster für konkurrierende Sperranfragen kleiner wird. Die Tabelle information_schema.innodb_trx und der Befehl SHOW ENGINE INNODB STATUS liefern Details zum letzten erkannten Deadlock, inklusive der beteiligten Statements.
6. Retry-Strategien für Optimistic-Locking-Konflikte
Ein erkannter Konflikt bei Optimistic Locking ist kein Fehlerfall im technischen Sinne, sondern ein normaler Ablaufzweig, den die Anwendung behandeln muss. Die gängige Strategie ist ein Retry mit erneutem Laden des aktuellen Datensatzes: Die Anwendung fängt die OptimisticLockException, lädt den Datensatz mit der neuen Version erneut, wendet die fachliche Änderung auf den aktuellen Stand an und versucht das Update erneut. Eine begrenzte Anzahl an Versuchen verhindert Endlosschleifen bei dauerhaft hoher Konkurrenz auf denselben Datensatz.
Bei Konflikten, die sich nicht automatisch auflösen lassen, etwa wenn zwei Nutzer denselben Text unabhängig voneinander bearbeitet haben, ist ein automatischer Retry fachlich riskant. Hier sollte die Anwendung den Konflikt an den Nutzer zurückmelden, statt eine Änderung stillschweigend zu verwerfen. Ein exponentielles Backoff zwischen den Retry-Versuchen reduziert zusätzlich die Last auf stark umkämpften Datensätzen.
<?php
declare(strict_types=1);
/**
* Retries an optimistic-locking update with exponential backoff.
*/
function updateWithRetry(ProductRepository $repo, int $id, float $newPrice, int $maxAttempts = 3): void
{
$attempt = 0;
while (true) {
$attempt++;
$product = $repo->find($id);
try {
$repo->updatePrice($id, $newPrice, (int) $product['version']);
return;
} catch (OptimisticLockException $e) {
if ($attempt >= $maxAttempts) {
throw $e;
}
usleep(50_000 * (2 ** $attempt)); // exponential backoff
}
}
}
7. Entscheidungskriterien: Welches Modell für welchen Fall
Die Wahl zwischen Optimistic Locking und Pessimistic Locking hängt maßgeblich von der Konfliktwahrscheinlichkeit ab. Bei niedriger Konfliktwahrscheinlichkeit, wenn also selten zwei Transaktionen denselben Datensatz gleichzeitig bearbeiten, ist Optimistic Locking fast immer die bessere Wahl, weil es keinen unnötigen Wartezustand erzeugt. Klassische Beispiele sind CMS-Artikel, Nutzerprofile und Formularbearbeitung, bei denen Konflikte die Ausnahme sind.
Bei hoher Konfliktwahrscheinlichkeit, wie bei begrenztem Lagerbestand während eines Sale-Events oder bei Kontoständen mit vielen parallelen Buchungen, überwiegt Pessimistic Locking die Vorteile, weil wiederholte Retries bei Optimistic Locking unter starker Konkurrenz selbst zum Performance-Problem werden können. Ein drittes Kriterium ist die Transaktionsdauer: Pessimistic Locking sollte nur bei kurzen Transaktionen eingesetzt werden, da lange gehaltene Sperren die Nebenläufigkeit des gesamten Systems einschränken.
8. Praxisbeispiel: Lagerbestand in einer E-Commerce-Anwendung
Ein typisches Praxisbeispiel für Pessimistic Locking ist die Reservierung von Lagerbestand während eines Checkout-Vorgangs. Mehrere Kunden könnten gleichzeitig das letzte verfügbare Stück eines Artikels in den Warenkorb legen. Ohne Sperre würden beide Transaktionen den verfügbaren Bestand unabhängig prüfen, beide eine ausreichende Menge feststellen und beide erfolgreich reservieren, obwohl nur ein Stück vorhanden ist. Das folgende Beispiel kombiniert SELECT ... FOR UPDATE mit einer Bedingung in der anschließenden UPDATE-Anweisung als zusätzliche Absicherung.
Für die zugehörige Produktbeschreibung, die deutlich seltener gleichzeitig bearbeitet wird, eignet sich hingegen Optimistic Locking mit einer Versionsspalte besser, weil Lesezugriffe im Produktkatalog nicht durch Schreibsperren blockiert werden sollen. Diese Kombination beider Strategien innerhalb derselben Anwendung ist üblich: Jede Tabelle erhält das Locking-Modell, das zu ihrem tatsächlichen Zugriffsmuster passt, statt pauschal eine einzige Strategie für die gesamte Datenbank zu verwenden.
-- Pessimistic Locking for high-contention inventory reservation
START TRANSACTION;
SELECT quantity, reserved
FROM inventory
WHERE product_id = 4711
FOR UPDATE;
UPDATE inventory
SET reserved = reserved + 1
WHERE product_id = 4711
AND (quantity - reserved) >= 1;
-- Check affected rows in the application; 0 means sold out
COMMIT;
9. Optimistic Locking und Pessimistic Locking im Vergleich
Beide Sperrstrategien lösen dasselbe fachliche Problem mit unterschiedlichen Trade-offs bei Durchsatz, Latenz und Implementierungsaufwand. Die folgende Tabelle fasst die wichtigsten Unterschiede zusammen und hilft bei der schnellen Einordnung im konkreten Projekt.
| Kriterium | Pessimistic Locking | Optimistic Locking |
|---|---|---|
| Mechanismus | SELECT ... FOR UPDATE, Sperre bis COMMIT |
Versionsspalte, Prüfung beim UPDATE |
| Konfliktbehandlung | Vorab durch Blockieren verhindert | Nachträglich erkannt, benötigt Retry |
| Ideal bei | Hoher Konfliktwahrscheinlichkeit | Niedriger Konfliktwahrscheinlichkeit |
| Durchsatz bei wenig Konkurrenz | Unnötige Wartezeit | Sehr hoch |
| Risiko | Deadlocks, blockierte Verbindungen | Häufige Retries bei hoher Konkurrenz |
| Implementierungsaufwand | Gering, direkt in SQL | Mittel, Retry-Logik in der Anwendung |
In der Praxis ist die Kombination beider Strategien innerhalb einer Anwendung normal und sinnvoll. Kritische, stark umkämpfte Ressourcen wie Lagerbestände profitieren von Pessimistic Locking, während der Großteil der Anwendung mit geringer Konfliktwahrscheinlichkeit von der besseren Skalierbarkeit des Optimistic Locking profitiert.
10. Zusammenfassung
Optimistic Locking und Pessimistic Locking lösen beide das Lost-Update-Problem bei gleichzeitigem Zugriff auf Datensätze, unterscheiden sich aber grundlegend im Zeitpunkt der Konflikterkennung. Pessimistic Locking mit SELECT ... FOR UPDATE verhindert Konflikte proaktiv durch Sperren und eignet sich für Szenarien mit hoher Konfliktwahrscheinlichkeit wie Lagerbestandsreservierungen. Optimistic Locking mit einer Versionsspalte erkennt Konflikte erst beim Schreiben und benötigt eine Retry-Strategie in der Anwendungsschicht, bietet dafür aber deutlich höheren Durchsatz bei geringer Konkurrenz.
Die richtige Entscheidung wird pro Tabelle und pro Anwendungsfall getroffen, nicht global für die gesamte Datenbank. Wer beide Strategien beherrscht und gezielt kombiniert, vermeidet sowohl unnötige Wartezeiten als auch stille Datenverluste durch überschriebene Änderungen. Konsistente Sperrreihenfolgen und kurze Transaktionen reduzieren zusätzlich das Deadlock-Risiko bei Pessimistic Locking, während begrenzte Retry-Versuche mit Backoff Endlosschleifen bei Optimistic Locking verhindern.
Optimistic vs. Pessimistic Locking, das Wichtigste auf einen Blick
Pessimistic Locking
SELECT ... FOR UPDATE sperrt Zeilen bis zum COMMIT. Ideal bei hoher Konfliktwahrscheinlichkeit und kurzen Transaktionen.
Optimistic Locking
Versionsspalte prüft beim UPDATE auf Änderungen. Kein Warten beim Lesen, dafür Retry-Logik bei Konflikten nötig.
Deadlocks vermeiden
Konsistente Sperrreihenfolge nach Primärschlüssel und kurze Transaktionsdauer reduzieren Deadlock-Risiko deutlich.
Kombination
Beide Modelle pro Tabelle einzeln wählen, je nach tatsächlichem Zugriffsmuster, nicht pauschal für die gesamte Datenbank.