Skip to content

How-to: Optimistische Nebenläufigkeitskontrolle (Versionsfeld)

FT-Referenz: FT323 (NENE2-FT/optimisticlog) — Dokument-API mit Versionsfeld im PUT-Body, 409 bei veralteter Version, Verhinderung verlorener Updates, 18 Tests / 34 Assertions PASS.

Diese Anleitung zeigt, wie optimistische Nebenläufigkeitskontrolle durch Übergabe eines version-Feldes im Request-Body implementiert wird, als Alternative zu HTTP-ETag/If-Match-Headern.

Schema

sql
CREATE TABLE documents (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    title      TEXT    NOT NULL,
    body       TEXT    NOT NULL DEFAULT '',
    version    INTEGER NOT NULL DEFAULT 1,
    updated_at TEXT    NOT NULL
);

Endpunkte

MethodePfadBeschreibung
POST/documentsErstellen (Version beginnt bei 1)
GET/documentsAuflisten
GET/documents/{id}Abrufen mit Version
PUT/documents/{id}Aktualisieren (Version im Body erforderlich)

Erstellen

php
POST /documents  {"title": "Hello", "body": "World"}
 201  {"id": 1, "title": "Hello", "version": 1}

Aktualisieren mit Version

Der Client liest die aktuelle version und schließt sie in den PUT-Body ein:

php
// Lesen
GET /documents/1
 200  {"id": 1, "title": "Hello", "version": 1}

// Aktualisieren mit korrekter Version
PUT /documents/1
{"title": "Updated", "body": "new body", "version": 1}
 200  {"id": 1, "title": "Updated", "version": 2}

Die Version wird bei jeder erfolgreichen Aktualisierung inkrementiert.

Veraltete Version — 409 Conflict

php
// Alice und Bob lesen beide Version 1
// Alice aktualisiert zuerst → Version wird 2
// Bob versucht mit Version 1 zu aktualisieren → abgelehnt
PUT /documents/1
{"title": "Bobs Bearbeitung", "version": 1}
 409 Conflict  {"current_version": 2, "submitted_version": 1}

// Bob liest erneut, erhält Version 2, versucht erneut
PUT /documents/1
{"title": "Bobs Bearbeitung", "version": 2}
 200  {"version": 3}

Implementierung

php
private function update(ServerRequestInterface $request): ResponseInterface
{
    $body    = $this->parseBody($request);
    $version = $body['version'] ?? null;

    if (!is_int($version) || $version < 1) {
        return $this->json->create(['error' => 'version is required'], 422);
    }

    $doc = $this->repo->findById($id);
    if ($doc === null) {
        return $this->json->create(['error' => 'Not found'], 404);
    }

    if ($doc['version'] !== $version) {
        return $this->problems->create('conflict', 'Stale version', 409, [
            'current_version'   => $doc['version'],
            'submitted_version' => $version,
        ]);
    }

    $newVersion = $version + 1;
    // UPDATE documents SET ... WHERE id = ? AND version = ?
    $this->repo->update($id, $title, $newVersion, $now);

    return $this->json->create($updated);
}

Die WHERE version = ?-Klausel in der UPDATE-Abfrage ist der atomare Guard gegen gleichzeitige Schreibvorgänge.

Version vs. ETag

AspektVersionsfeld (diese Anleitung)ETag / If-Match (siehe optimistic-locking-etag.md)
ProtokollBody-FeldHTTP-Header
Client-UXExplizites "version": N in JSONIf-Match: "vN"-Header
409-PayloadKann current_version zurückgeben412 — kein Body-Standard
Fehlende Prüfung422 (fehlendes version)428 (fehlendes If-Match)

Was man NICHT tun sollte

Anti-MusterRisiko
PUT ohne version-Feld akzeptierenVerlorene Updates: letztes Schreiben gewinnt stillschweigend
200 bei veralteter Version zurückgebenStille Überschreibung gleichzeitiger Änderungen
Version nur im Anwendungscode prüfen (nicht in WHERE-Klausel)Race Condition zwischen Lesen und Schreiben
current_version nicht in 409-Antwort einschließenClient muss neu-GET, um sich zu erholen; für schnelleren Neuversuch einschließen

Veröffentlicht unter der MIT-Lizenz.