Optimistisches vs. pessimistisches Locking in Datenbanken
Zwoa Requests lesn dieselbe Zeiln, beide berechnen aus dem Gelesenen an neichn Wert und beide schreibn ihn zruck - der zwoate Schreibvorgang überschreibt den ersten still und leise, ois wär er nie passiert. Des is a Lost Update, und es is der Grund, warum nebenläufiger Datenbankzugriff irgendeine Form von Locking braucht. De zwoa Standardstrategien, optimistisch und pessimistisch, wetten gegensätzlich drauf, wie oft Konflikte tatsächlich vorkemman.
Des Lost Update, konkret
Sogn ma, du reduzierst beim Checkout den Lagerbestand.
-- request A reads stock = 5
SELECT stock FROM products WHERE id = 42;
-- request B reads stock = 5, at nearly the same time
SELECT stock FROM products WHERE id = 42;
-- both compute 5 - 1 = 4 and write it back
UPDATE products SET stock = 4 WHERE id = 42;
UPDATE products SET stock = 4 WHERE id = 42;
Zwoa Stück verkauft, aber der Bestand is nur um oans gsunken. Koa Query is gscheitert, koa Transaktion zruckgrollt - die Daten sand einfach falsch. Locking gibt's, damit oana von den zwoa Requests auf den ondern wartn muass, oda dagegn scheitert.
Pessimistisches Locking: Annehmen, dass a Konflikt kummt
Pessimistisches Locking schnappt si an Lock auf die Zeiln, bevor du sie anfasst, und hoitet ihn bis zum End der Transaktion, wobei jede ondere Transaktion blockiert wird, de den söbn Lock will.
BEGIN;
SELECT stock FROM products WHERE id = 42 FOR UPDATE;
-- stock is now locked until this transaction commits or rolls back
UPDATE products SET stock = stock - 1 WHERE id = 42;
COMMIT;
FOR UPDATE (Postgres, MySQL/InnoDB und die meisten ondern relationalen Engines unterstützen des oda a Äquivalent) sagt der Datenbank: Jede ondere Transaktion, de diese Zeiln mit FOR UPDATE lesn oda updaten will, muass warten, bis diese fertig is. Request B oben stellt si einfach hinter Request A an, statt mit ihm zu rennen.
Des is einfach zum Durchdenkn - es gibt koane Möglichkeit für a Lost Update, Punkt -, aber es hot seinen Preis: Jede gsperrte Zeiln is a Contention-Punkt. Wenn dein Checkout-Ablauf den Lock über an langsamen Zahlungs-API-Aufruf hoit, wartet jeder ondere, der des söbe Produkt kaufn wü, a drauf.
Optimistisches Locking: Annehmen, dass a Konflikt selten is
Optimistisches Locking sperrt vorab gor nix. Stattdessen markiert es jede Zeiln mit ana Versionsnummer und prüft beim Schreibn, dass sich die Version ned gändert hot.
-- add a version column
ALTER TABLE products ADD COLUMN version INT NOT NULL DEFAULT 0;
-- read the row along with its current version
SELECT stock, version FROM products WHERE id = 42;
-- suppose stock = 5, version = 7
-- write back conditioned on the version being unchanged
UPDATE products
SET stock = 4, version = 8
WHERE id = 42 AND version = 7;
Prüf die Anzoi der betroffenen Zeiln vom UPDATE. Wenn's 1 is, war dein Schreibvorgang erfolgreich und niemand sonst hot die Zeiln dazwischen angfasst. Wenn's 0 is, hot wer ondrer die Zeiln zwischn deim Lesn und Schreibn aktualisiert (und ihre Version erhöht) - deine Daten sand veraltet, und die Anwendung muass entscheiden, wos zu tun is: neu lesn und wiederholn, oda an Konflikt dem Nutzer zeign.
async function decrementStock(productId) {
const { stock, version } = await db.one(
'SELECT stock, version FROM products WHERE id = $1',
[productId],
);
const result = await db.result(
'UPDATE products SET stock = $1, version = $2 WHERE id = $3 AND version = $4',
[stock - 1, version + 1, productId, version],
);
if (result.rowCount === 0) {
throw new ConflictError('Product was modified concurrently, retry');
}
}
Nie wird a Lock über Anwendungslogik, Netzwerkaufrufe oda Nutzer-Nachdenkzeit gehoitn. Contention wird erst im Moment vom Schreibn sichtbar, und nur dann, wenn tatsächlich a Konflikt passiert is.
Zwischen den beiden wählen
- Pessimistisch, wenn Konflikte häufig und billig zum serialisiern sand - a klane Zoi an heißen Zeiln (der Bestand von oam beliebten Produkt, a geteilter Zähler, a Sitzplatz in am Buchungssystem), wo du Requests lieber wartn lasst, ois sie wiederholn zum lassn.
- Optimistisch, wenn Konflikte selten sand und die Orbeit zwischn Lesn und Schreibn teuer oda langwierig is - a Dokument bearbeiten, a Nutzerprofil aktualisiern, olles, wo a Mensch zwischn Lesn und Schreibn a Formular ausfüllt. A Datenbank-Lock für die Dauer von "Nutzer überlegt grod" zu hoitn, is ned machbar.
Eine nützliche Faustregel: Wennst um an Netzwerkaufruf, a UI-Interaktion oda irgendwos, des koa knappe Datenbanktransaktion is, herumsperrst, willst fast sicher optimistisches Locking.
Den Konflikt gut behandeln
A 0-Zeilen-Update vom optimistischen Locking is ka Fehlerzustand zum Verschluckn - es is Information. Drei übliche Wege, damit umzugehn:
- Wiederholn: Zeiln neu lesn, Änderung neu anwendn, Schreibvorgang nochamoi probiern. Funktioniert gut bei einfachen numerischen Updates wia dem Lagerbestand oben, weil's neu lesn dir an frischen, korrekten Startpunkt gibt.
- Zusammenführen: Wenn das Update komplexer is (a Dokument mit mehreren Feldern bearbeiten), neu lesn und die nicht-konfliktierenden Teile zammführn, dann wiederholn.
- Zeigen: Bei nutzerseitigen Bearbeitungen dem Nutzer sogn "des wurde seit dem Laden von wem ondern gändert" und ihn entscheiden lossn, statt still zu überschreiben oda still seine Orbeit zu verwerfen.
Wos du nie machn soitst, is den Fall mit der Null-Zeiln-Anzoi zum Abfangen und einfach weiterzumachn - des wär wieda das Lost Update, nur mit zusätzlichen Schritten.
Fazit
Pessimistisches Locking verhindert Konflikte, indem's nebenläufigen Zugriff auf seine Reih wartn lasst; optimistisches Locking erkennt Konflikte im Nachhinein und überlasst es der Anwendung, darauf zu reagiern. Koans von beiden is universell besser - es kummt drauf an, wie oft du Kollisionen erwartest, und wie teuer es is, an Request wartn zu lassn versus wiederholn.