← Zurück

Kapitelkompass

Nebenläufigkeit als Zustandsproblem lesen

Isolation entscheidet, welche Zwischenzustände andere Transaktionen sehen und wie Konflikte behandelt werden.

LesenAusgangspunktKonfliktEinordnenErkennenAbsichernNeu versuchenErgebnis
Der Themenweg zeigt die fachliche Leserichtung dieses Kapitels.

Das nimmst du mit

  • Anomalien unterscheiden
  • Optimistic Locking einsetzen
  • Deadlocks reproduzieren
  • Retry sicher begrenzen

Praxisfall

Zwei Bearbeiter aktualisieren dieselbe Bestellung in unterschiedlicher Reihenfolge.

Entscheidung

Optimistisch bei seltenen Konflikten; pessimistisch nur bei begründeter hoher Konkurrenz.

!

Typisches Risiko

Blindes Retry kann fachliche Duplikate erzeugen und Deadlocks nur zeitlich verschieben.

03. Locking, Isolation und Deadlocks

1. Warum Locking ein fachliches Thema ist

Locking wird in Projekten oft zu spät betrachtet, weil es nach Datenbankdetail klingt. In Wahrheit entscheidet Locking darüber, ob fachliche Zusagen stimmen: ob ein Lagerbestand wirklich reserviert ist, ob eine Rechnung nur einmal erzeugt wird, ob eine Zahlung nicht doppelt bestätigt wird und ob zwei Support-Mitarbeiter dieselbe Bestellung gleichzeitig widersprüchlich ändern können.

In Kapitel ging es um Transaktionsgrenzen. Kapitel geht eine Ebene tiefer: Was passiert, wenn mehrere Transaktionen gleichzeitig auf dieselben Daten zugreifen? Ohne klares Modell entstehen Fehler, die im Unit Test selten auffallen, im Betrieb aber sehr teuer werden.

Merksatz
Eine Transaktion schützt nicht automatisch vor falscher Konkurrenzlogik. Sie sorgt für Atomarität innerhalb einer Grenze. Konsistenz bei parallelen Zugriffen braucht zusätzlich passende Isolation, Locking, Versionen und fachliche Konfliktbehandlung.

Typische Symptome in echten Enterprise-Systemen

Inventory Reservation zeigt, warum Locking nicht abstrakt bleiben darf.

2. Lost Update: der klassische Fehler

Lost Update ist einer der wichtigsten Konkurrenzfehler. Zwei Transaktionen lesen denselben alten Zustand, berechnen beide einen neuen Zustand und schreiben nacheinander zurück. Die spätere Transaktion überschreibt die frühere, ohne zu merken, dass ihre Grundlage veraltet war.

Die visuelle Abfolge zeigt, warum der finale Datenbankwert plausibel aussehen kann, obwohl die fachliche Wahrheit falsch ist.

Schlechte Lösung: Read - Calculate - Write ohne Version

Die folgende Klasse ist absichtlich schlecht. Sie wird im Lab benutzt, um den Fehler sichtbar zu machen. Der Fehler liegt nicht darin, dass die Klasse kurz ist, sondern darin, dass der Snapshot beim Commit nicht mehr validiert wird.

NaiveReservationSession.java - bewusstes Gegenbeispiel

JAVA
package com.example.lockdeepdive.inventory;
// Pattern: Gegenbeispiel - diese Klasse zeigt bewusst den Lost-Update-Fehler.
public final class NaiveReservationSession {
    private final InventoryTable table;
    private final StockSnapshot snapshot;
    private Integer plannedQuantity;
    public NaiveReservationSession(InventoryTable table, StockSnapshot snapshot) {
        this.table = table;
        this.snapshot = snapshot;
    }
public boolean planReservation(int requested) {
    if (requested <= 0) throw new IllegalArgumentException("requested must be positive");
    if (snapshot.quantity() < requested) return false;
    plannedQuantity = snapshot.quantity() - requested;
    return true;
}
public void commitWithoutVersionCheck() {
    if (plannedQuantity == null) throw new IllegalStateException("nothing planned");
    table.writeWithoutVersionCheck(snapshot.productId(), plannedQuantity);
}
public StockSnapshot snapshot() {
    return snapshot;
}
}

Warum das im Enterprise-Projekt gefährlich ist

Situation Technisch sichtbar Fachlicher Schaden
beide Transaktionen committen kein Fehler Bestand überverkauft
letzter Writer gewinnt finale Zahl wirkt plausibel erste Reservierung ist unsichtbar verloren
nachgelagerte Events gesendet Broker meldet OK Reporting und Kundennachricht widersprechen Lager

3. Optimistic Locking: Konflikte erkennen statt lange sperren

Optimistic Locking basiert auf der Annahme: Konflikte sind möglich, aber nicht der Normalfall. Deshalb hält das System beim Lesen keine lange Sperre. Stattdessen wird beim Schreiben geprüft, ob sich die Version seit dem Lesen verändert hat.

Versionierte Updates machen veraltete Entscheidungen sichtbar.

SQL-Mentalmodell

Optimistic Locking als SQL-Mentalmodell

SQL
UPDATE inventory
 SET quantity = ?, version = version + 1
 WHERE product_id = ?
 AND version = ?;
0 rows updated -> jemand war schneller; Konflikt behandeln
1 row updated -> Commit kann fortgesetzt werden

Bessere Lösung: Use Case lädt nach Konflikt neu

Der wichtige Punkt ist nicht nur der Version Check. Der Use Case muss nach einem Konflikt den neuen Zustand lesen und die fachliche Regel erneut prüfen. Ein technischer Retry ohne fachliche Neubewertung ist gefährlich.

OptimisticInventoryService.java

JAVA
package com.example.lockdeepdive.inventory;
import com.example.lockdeepdive.shared.*;
// Pattern: Application Service - kapselt Use-Case-Regeln und technische Konfliktbehandlung.
public final class OptimisticInventoryService {
    private final InventoryTable table;
    private final int maxRetries;
    public OptimisticInventoryService(InventoryTable table, int maxRetries) {
        this.table = table;
        this.maxRetries = maxRetries;
    }
public ReservationResult reserve(ProductId productId, int requested) {
    for (int attempt = 1; attempt <= maxRetries; attempt++) {
        StockSnapshot current = table.read(productId);
        if (current.quantity() < requested) {
            return new ReservationResult.Rejected(productId, requested, current.quantity());
        }
    int newQuantity = current.quantity() - requested;
    try {
        StockSnapshot saved = table.writeIfVersionMatches(productId, current.version(), newQuantity);
        return new ReservationResult.Reserved(productId, requested, saved.quantity(), saved.version());
    }
catch (OptimisticLockException conflict) {
    if (attempt == maxRetries) {
        return new ReservationResult.Conflict(productId, conflict.getMessage());
    }
// In echter Produktion: kleiner Backoff + Metrik + erneute fachliche Pruefung.
}
}
return new ReservationResult.Conflict(productId, "retry loop exhausted");
}
}

Wann Optimistic Locking gut passt

Wann Vorsicht nötig ist

4. Isolation Levels: was die Transaktion sehen darf

Isolation beschreibt, welche Zwischeneffekte paralleler Transaktionen sichtbar sind. Sie ist nicht nur Performance-Einstellung, sondern beeinflusst fachliche Aussagen wie „Ich habe gerade geprüft, dass genug Bestand vorhanden ist“ oder „Diese Rechnung wurde noch nicht erzeugt“.

Die Matrix fasst typische Phänomene zusammen.

Read Committed

Read Committed verhindert, dass uncommittete Änderungen anderer Transaktionen gelesen werden. Aber dieselbe Query kann innerhalb einer Transaktion später ein anderes Ergebnis liefern, wenn inzwischen jemand anderes committed hat.

Repeatable Read

Repeatable Read stabilisiert bereits gelesene Zeilen innerhalb derselben Transaktion. Das hilft bei wiederholten Prüfungen, löst aber nicht automatisch alle Bereichsprobleme, insbesondere abhängig von Datenbank und konkreter Abfrageform.

Serializable

Serializable ist das strengste Modell. Es versucht, parallele Ausführungen so wirken zu lassen, als wären sie nacheinander passiert. Dafür können mehr Sperren, mehr Konflikte oder Abbrüche entstehen. In Enterprise-Systemen nutzt man es gezielt, nicht reflexartig überall.

SimulatedTransaction.java - Read Committed vs Repeatable Read im Lab

JAVA
package com.example.lockdeepdive.isolation;
import java.util.*;
// Pattern: Unit of Work Mental Model - Transaktion merkt sich je nach Isolation gelesene Snapshots.
public final class SimulatedTransaction {
    private final AccountTable table;
    private final IsolationLevel isolationLevel;
    private final Map<AccountId, AccountSnapshot> repeatableReadCache = new HashMap<>();
    public SimulatedTransaction(AccountTable table, IsolationLevel isolationLevel) {
        this.table = table;
        this.isolationLevel = isolationLevel;
    }
public AccountSnapshot read(AccountId id) {
    if (isolationLevel == IsolationLevel.REPEATABLE_READ) {
        return repeatableReadCache.computeIfAbsent(id, table::committedRead);
    }
return table.committedRead(id);
}
public IsolationLevel isolationLevel() {
    return isolationLevel;
}
}
Phänomen Bedeutung Beispiel
Dirty Read Lesen uncommitteter Daten Order sieht Payment-Status, der später rollbackt
Non-repeatable Read dieselbe Zeile liefert später anderen Wert Support liest Order zweimal und sieht verschiedene Stati
Phantom Read Bereichsabfrage liefert neue/fehlende Zeilen Batch findet beim zweiten Scan zusätzliche Rechnungen
Lost Update Write überschreibt anderen Write Inventory-Reservierung überschreibt parallele Reservierung

5. Pessimistic Locking: bewusst sperren, aber kurz

Pessimistic Locking basiert auf der Annahme: Konflikte sind wahrscheinlich oder teuer. Deshalb wird der kritische Datensatz gesperrt, bevor die fachliche Entscheidung getroffen und gespeichert wird. Das kann korrekt und notwendig sein - aber nur mit sehr kurzen Transaktionen.

Pessimistische Sperren müssen so kurz wie möglich gehalten werden.

SELECT FOR UPDATE als Mentalmodell

Pessimistic Locking in SQL-Denkweise

SQL
BEGIN;
SELECT quantity, version
 FROM inventory
 WHERE product_id = 'SKU-1'
 FOR UPDATE;
-- jetzt: pruefen, reduzieren, Outbox schreiben
COMMIT;

Lock Manager im JDK-only-Lab

RowLockManager.java

JAVA
package com.example.lockdeepdive.locking;
import com.example.lockdeepdive.shared.ProductId;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.locks.*;
// Pattern: Lock Manager - simuliert DB-Zeilensperren mit Timeout.
public final class RowLockManager {
    private final ConcurrentMap<ProductId, ReentrantLock> locks = new ConcurrentHashMap<>();
    public Guard lock(ProductId productId, Duration timeout) {
        ReentrantLock lock = locks.computeIfAbsent(productId, id -> new ReentrantLock());
        try {
            if (!lock.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
                throw new LockTimeoutException("timeout while waiting for lock on " + productId);
            }
        return new Guard(productId, lock);
    }
catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new LockTimeoutException("interrupted while waiting for lock on " + productId);
}
}
public record Guard(ProductId productId, ReentrantLock lock) implements AutoCloseable {
    @Override public void close() {
        lock.unlock();
    }
}
}

Use Case mit kurzer kritischer Sektion

PessimisticInventoryService.java

JAVA
package com.example.lockdeepdive.locking;
import com.example.lockdeepdive.inventory.*;
import com.example.lockdeepdive.shared.*;
import java.time.Duration;
// Pattern: Application Service + Pessimistic Lock - kurze kritische Sektion, keine externen API-Aufrufe im Lock.
public final class PessimisticInventoryService {
    private final InventoryTable table;
    private final RowLockManager locks;
    private final Duration timeout;
    public PessimisticInventoryService(InventoryTable table, RowLockManager locks, Duration timeout) {
        this.table = table;
        this.locks = locks;
        this.timeout = timeout;
    }
public ReservationResult reserve(ProductId productId, int requested) {
    try (RowLockManager.Guard ignored = locks.lock(productId, timeout)) {
        StockSnapshot current = table.read(productId);
        if (current.quantity() < requested) {
            return new ReservationResult.Rejected(productId, requested, current.quantity());
        }
    StockSnapshot saved = table.writeIfVersionMatches(productId, current.version(), current.quantity() - requested);
    return new ReservationResult.Reserved(productId, requested, saved.quantity(), saved.version());
}
catch (LockTimeoutException timeout) {
    return new ReservationResult.Conflict(productId, timeout.getMessage());
}
}
}

Was niemals in die Sperre gehört

Produktionsregel
Eine Sperre schützt den kritischen Zustand. Sie darf nicht benutzt werden, um beliebig lange Nebenwirkungen bequem in einer Transaktion zu halten.

6. Deadlocks verstehen und vermeiden

Ein Deadlock entsteht, wenn Transaktionen zyklisch auf Ressourcen warten. Klassisch: Transaktion A hält Lock 1 und wartet auf Lock 2, Transaktion B hält Lock 2 und wartet auf Lock 1. Ohne Timeout oder Deadlock Detection kann keine der beiden fortfahren.

Der Wait-for Graph macht den Kreis sichtbar.

Echte Ursachen

Deterministische Deadlock-Simulation

DeadlockSimulator.java

JAVA
package com.example.lockdeepdive.deadlock;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
// Pattern: Deterministic Concurrency Test Helper - erzwingt gegensaetzliche Lock-Reihenfolge reproduzierbar.
public final class DeadlockSimulator {
    private final ReentrantLock productOne = new ReentrantLock();
    private final ReentrantLock productTwo = new ReentrantLock();
    public DeadlockSimulationResult simulateOppositeLockOrder(Duration timeout) {
        CountDownLatch bothHoldFirstLock = new CountDownLatch(2);
        ExecutorService executor = Executors.newFixedThreadPool(2);
        try {
            Future<Boolean> first = executor.submit(() -> worker(productOne, productTwo, bothHoldFirstLock, timeout));
            Future<Boolean> second = executor.submit(() -> worker(productTwo, productOne, bothHoldFirstLock, timeout));
            boolean firstTimedOut = first.get(5, TimeUnit.SECONDS);
            boolean secondTimedOut = second.get(5, TimeUnit.SECONDS);
            return new DeadlockSimulationResult(firstTimedOut, secondTimedOut,
            "opposite lock order created a wait cycle; timeout broke at least one worker");
        }
    catch (Exception e) {
        throw new IllegalStateException("deadlock simulation failed", e);
    }
finally {
    executor.shutdownNow();
}
}
private boolean worker(ReentrantLock first, ReentrantLock second, CountDownLatch latch, Duration timeout) throws Exception {
    first.lock();
    try {
        latch.countDown();
        if (!latch.await(2, TimeUnit.SECONDS)) throw new IllegalStateException("test setup failed");
        boolean acquiredSecond = second.tryLock(timeout.toMillis(), TimeUnit.MILLISECONDS);
        if (!acquiredSecond) return true;
        try {
            return false;
        }
    finally {
        second.unlock();
    }
}
finally {
    first.unlock();
}
}
}

Gegenmaßnahmen

Maßnahme Warum sie hilft Grenze
feste Lock-Reihenfolge verhindert Kreise im Wait-for Graph muss in allen Codepfaden gelten
kurze Transaktionen reduziert Wartezeit und Konfliktfenster braucht klare Use-Case-Schnitte
Lock Timeout System bleibt reaktionsfähig Fehler muss fachlich/technisch behandelt werden
Retry mit Backoff transiente Konflikte können verschwinden darf keine doppelten Nebenwirkungen erzeugen
gute Indizes Datenbank sperrt weniger unnötige Zeilen Query-Pläne müssen beobachtet werden

7. Retry nach Konflikt: nicht blind wiederholen

Retry ist kein magischer Korrektheitsmechanismus. Ein Retry ist nur dann sicher, wenn der Use Case idempotent ist oder Nebenwirkungen sauber isoliert sind. Bei Optimistic Locking muss der neue Zustand geladen und die fachliche Regel neu bewertet werden.

Die Entscheidung nach einem Konflikt ist fachlich, nicht nur technisch.

Guter Retry

Schlechter Retry

Faustregel
Retry darf eine technische Kollision überbrücken. Retry darf keine fachliche Entscheidung erzwingen, die nach dem neuen Zustand nicht mehr gültig ist.

8. JPA/Hibernate-Bezug: @Version, LockMode und Fetch-Pläne

Dieses Kapitel-Lab bleibt JDK-only, aber die Mechanik entspricht typischen JPA/Hibernate-Fragen. @Version ist das Standardwerkzeug für Optimistic Locking. Pessimistic Locking wird über Lock Modes oder Query-Hints angestoßen. Isolation kommt weiterhin aus Datenbank und Transaktionskonfiguration.

@Version Mentalmodell

JPA @Version als Optimistic-Locking-Werkzeug

JAVA
@Entity
class InventoryItemEntity {
    @Id String productId;
    int quantity;
    @Version
    long version;
}
-- Hibernate-Update enthaelt typischerweise eine Versionsbedingung.

Typische Fehler mit JPA

LockMode richtig denken

Lock Mode Gedanke Einsatz
OPTIMISTIC Konflikt beim Commit/Flush erkennen Benutzer- und Aggregate-Updates
OPTIMISTIC_FORCE_INCREMENT Version bewusst erhöhen fachliche Reservierung/Anspruch sichtbar machen
PESSIMISTIC_WRITE Schreibkonflikte durch Sperre verhindern hochkonkurrierte Ressourcen
PESSIMISTIC_READ Änderung durch andere erschweren spezielle Lesekonsistenzfälle

9. Tests: Konkurrenzfehler reproduzierbar machen

Concurrency-Fehler sind schwer, wenn Tests zufällig sind. Das Lab nutzt deshalb deterministische Simulationen: veraltete Snapshots werden kontrolliert erzeugt, Isolation wird über eine kleine Transaktionsklasse modelliert, Deadlock wird über Latches und Timeouts reproduzierbar gemacht.

Kapitel2TestRunner.java - Szenario-Tests

JAVA
package com.example.lockdeepdive;
import com.example.lockdeepdive.deadlock.*;
import com.example.lockdeepdive.inventory.*;
import com.example.lockdeepdive.isolation.*;
import com.example.lockdeepdive.locking.*;
import com.example.lockdeepdive.shared.*;
import java.time.Duration;
public final class Kapitel2TestRunner {
    public static void main(String[] args) {
        lostUpdateIsVisibleWithoutVersionCheck();
        optimisticLockDetectsStaleWrite();
        optimisticServiceRechecksBusinessRuleAfterConflict();
        pessimisticLockSerializesCriticalSection();
        readCommittedAllowsNonRepeatableRead();
        repeatableReadKeepsStableSnapshot();
        deadlockIsBrokenByTimeout();
        System.out.println("Kapitel2_TESTS_OK");
    }
static void lostUpdateIsVisibleWithoutVersionCheck() {
    ProductId sku = ProductId.of("SKU-1");
    InventoryTable table = new InventoryTable();
    table.insert(sku, 10);
    NaiveReservationSession txA = new NaiveReservationSession(table, table.read(sku));
    NaiveReservationSession txB = new NaiveReservationSession(table, table.read(sku));
    assertTrue(txA.planReservation(7), "txA can reserve 7");
    assertTrue(txB.planReservation(6), "txB can reserve 6 based on stale snapshot");
    txA.commitWithoutVersionCheck();
    txB.commitWithoutVersionCheck();
    assertEquals(4, table.quantity(sku), "last writer wins although 13 items were promised from stock 10");
}
static void optimisticLockDetectsStaleWrite() {
    ProductId sku = ProductId.of("SKU-2");
    InventoryTable table = new InventoryTable();
    table.insert(sku, 10);
    StockSnapshot a = table.read(sku);
    StockSnapshot b = table.read(sku);
    table.writeIfVersionMatches(sku, a.version(), 3);
    assertThrows(OptimisticLockException.class, () -> table.writeIfVersionMatches(sku, b.version(), 4),
    "stale writer must be rejected");
}
static void optimisticServiceRechecksBusinessRuleAfterConflict() {
    ProductId sku = ProductId.of("SKU-3");
    InventoryTable table = new InventoryTable();
    table.insert(sku, 10);
    OptimisticInventoryService service = new OptimisticInventoryService(table, 3);
    ReservationResult first = service.reserve(sku, 7);
    ReservationResult second = service.reserve(sku, 6);
    assertTrue(first.success(), "first reservation succeeds");
    assertTrue(!second.success(), "second reservation is rejected after reread because only 3 remain");
}
static void pessimisticLockSerializesCriticalSection() {
    ProductId sku = ProductId.of("SKU-4");
    InventoryTable table = new InventoryTable();
    table.insert(sku, 10);
    PessimisticInventoryService service = new PessimisticInventoryService(table, new RowLockManager(), Duration.ofMillis(500));
    ReservationResult first = service.reserve(sku, 6);
    ReservationResult second = service.reserve(sku, 5);
    assertTrue(first.success(), "first reservation succeeds");
    assertTrue(!second.success(), "second reservation sees updated remaining stock and fails fachlich");
    assertEquals(4, table.quantity(sku), "stock remains consistent");
}
static void readCommittedAllowsNonRepeatableRead() {
    AccountId account = AccountId.of("A-1");
    AccountTable table = new AccountTable();
    table.insert(account, 100);
    SimulatedTransaction tx = new SimulatedTransaction(table, IsolationLevel.READ_COMMITTED);
    int first = tx.read(account).balance();
    table.update(account, 150);
    int second = tx.read(account).balance();
    assertEquals(100, first, "first read");
    assertEquals(150, second, "second read sees newer committed value");
}
static void repeatableReadKeepsStableSnapshot() {
    AccountId account = AccountId.of("A-2");
    AccountTable table = new AccountTable();
    table.insert(account, 100);
    SimulatedTransaction tx = new SimulatedTransaction(table, IsolationLevel.REPEATABLE_READ);
    int first = tx.read(account).balance();
    table.update(account, 150);
    int second = tx.read(account).balance();
    assertEquals(100, first, "first read");
    assertEquals(100, second, "repeatable read keeps snapshot stable inside transaction");
}
static void deadlockIsBrokenByTimeout() {
    DeadlockSimulationResult result = new DeadlockSimulator().simulateOppositeLockOrder(Duration.ofMillis(200));
    assertTrue(result.deadlockWasBrokenByTimeout(), "timeout should break wait cycle");
}
static void assertTrue(boolean condition, String message) {
    if (!condition) throw new AssertionError(message);
}
static void assertEquals(int expected, int actual, String message) {
    if (expected != actual) throw new AssertionError(message + ": expected=" + expected + ", actual=" + actual);
}
static void assertThrows(Class<? extends Throwable> type, Runnable action, String message) {
    try {
        action.Kapitel();
    }
catch (Throwable t) {
    if (type.isInstance(t)) return;
    throw new AssertionError(message + ": wrong exception " + t);
}
throw new AssertionError(message + ": expected exception " + type.getSimpleName());
}
}

Was gute Tests hier prüfen

Teststrategie
Concurrency-Tests sollen nicht beweisen, dass Threads immer gleich laufen. Sie sollen kritische Reihenfolgen gezielt erzwingen und dadurch das fachliche Risiko erklären.

10. Produktionshinweise: Metriken, Timeouts, Indizes

Locking-Probleme werden oft erst im Betrieb sichtbar. Deshalb gehören Metriken, Logging und Datenbankbeobachtung zur Lösung. Wer nur Code schreibt, aber keine Konflikte misst, sieht die wichtigsten Signale zu spät.

Metriken

Logging

Index- und Query-Praxis

Fehlende Indizes können Locking drastisch verschlechtern. Wenn eine Datenbank für eine Änderung viele Zeilen scannen muss, sperrt sie oft mehr und länger als fachlich erwartet. Deshalb gehört Query-Plan-Prüfung zu Locking-Design.

Check Frage
Transaktionsdauer Wie lange bleibt die Transaktion offen?
Lock-Reihenfolge Greifen alle Codepfade in derselben Reihenfolge zu?
Timeouts Gibt es klare Obergrenzen?
Retry Ist Retry idempotent und begrenzt?
Indizes Findet die DB gezielt die betroffenen Zeilen?
User Experience Kann die UI Konflikte verständlich erklären?

11. Code-Lab Wegweiser

Das Code-Lab ist bewusst ohne externe Datenbank gebaut. Dadurch kann man die Mechanik lesen, kompilieren und ausführen. Es ersetzt keine echte Datenbanktests, aber es macht die Ursache der Fehler sichtbar.

Projektstruktur

SQL
code/locking-isolation-deep-dive-lab/
├── src/main/java/com/example/lockdeepdive/
 ├── inventory/ Lost Update, Optimistic Locking
 ├── isolation/ Read Committed vs Repeatable Read
 ├── locking/ RowLockManager, Pessimistic Locking
 ├── deadlock/ deterministische Deadlock-Simulation
 └── demo/ Demo Runner
├── src/test/java/ Kapitel2TestRunner
└── docs/ Design Patterns und ADRs

Kompilieren und ausführen

Befehle im Lab-Ordner

TEXT
javac --release 21 -d target/classes $(find src/main/java src/test/java -name "*.java")
java -cp target/classes com.example.lockdeepdive.demo.Kapitel2Demo
java -cp target/classes com.example.lockdeepdive.Kapitel2TestRunner

12. Checkliste für echte Projekte

Weiter
Kapitel vertieft Outbox, Idempotenz, Retry und Crash-Fälle. Dort wird aus Locking-Korrektheit eine robuste Integrationsstrategie.

⌂ Cockpit