Skip to content

Compendium chapter 02 architecture

github-actions[bot] edited this page Sep 23, 2026 · 1 revision

Navigation: Home > Pages

Kapitel 2: Architektur-Überblick

"Eine gute Architektur ist wie ein gutes Fundament - unsichtbar, aber entscheidend fΓΌr alles, was darauf aufbaut."


Überblick

In diesem Kapitel tauchen wir tief in die Architektur von ThemisDB ein. Sie lernen, wie die verschiedenen Schichten zusammenarbeiten, wie Transaktionen garantiert werden, und wie MVCC (Multi-Version Concurrency Control) ParallelitΓ€t ohne Locks ermΓΆglicht.

Was Sie in diesem Kapitel lernen werden:

  • Die Schichten-Architektur von ThemisDB
  • Wie MVCC funktioniert und warum es wichtig ist
  • Transaction Management und ACID-Garantien
  • Storage Layer mit RocksDB
  • Indexstrukturen fΓΌr Performance
  • Praktische Demonstration mit einer Todo-App

Voraussetzungen: Kapitel 1 gelesen, GrundverstΓ€ndnis von Datenbanken.


2.1 Die Schichten-Architektur

ThemisDB folgt einer klassischen Schichten-Architektur, wobei jede Schicht klare Verantwortlichkeiten hat.

Die fΓΌnf Schichten

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         1. Query Layer (AQL)                 β”‚
β”‚  β€’ Query Parsing & Optimization              β”‚
β”‚  β€’ Execution Planning                        β”‚
β”‚  β€’ Result Formatting                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     2. Transaction Manager (MVCC)            β”‚
β”‚  β€’ Snapshot Isolation                        β”‚
β”‚  β€’ Conflict Detection                        β”‚
β”‚  β€’ Commit/Rollback Coordination              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     3. Model Engines                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Graph    β”‚ Document β”‚ Vector           β”‚  β”‚
β”‚  β”‚ Engine   β”‚ Engine   β”‚ Engine           β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β€’ Model-spezifische Logik                   β”‚
β”‚  β€’ Secondary Indexes                         β”‚
β”‚  β€’ Constraints                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     4. Index Manager                         β”‚
β”‚  β€’ B-Tree Indexes                            β”‚
β”‚  β€’ Hash Indexes                              β”‚
β”‚  β€’ Geo Indexes (Hilbert Curves)              β”‚
β”‚  β€’ Vector Indexes (HNSW)                     β”‚
β”‚  β€’ Fulltext Indexes                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     5. Storage Layer (RocksDB)               β”‚
β”‚  β€’ LSM-Trees fΓΌr SSD-Optimierung             β”‚
β”‚  β€’ Write-Ahead Log (WAL)                     β”‚
β”‚  β€’ Compression (LZ4, Zstandard)              β”‚
β”‚  β€’ Snapshots & Backups                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Warum diese Aufteilung?

Separation of Concerns: Jede Schicht hat eine klar definierte Aufgabe und kann unabhΓ€ngig optimiert oder ersetzt werden.

Beispiel: Wenn wir in Zukunft eine neue Storage-Engine einfΓΌhren wollen, mΓΌssen wir nur Schicht 5 Γ€ndern - alle anderen Schichten bleiben unverΓ€ndert.


2.2 MVCC: ParallelitΓ€t ohne Locks

Das Problem mit Locks

Traditionelle Datenbanken verwenden Locks:

# Traditionell: Pessimistic Locking
transaction1.begin()
transaction1.lock(row_id)  # Row wird gesperrt
transaction1.update(row_id, new_value)
transaction1.unlock(row_id)
transaction1.commit()

# Problem: Transaction 2 muss warten
transaction2.begin()
transaction2.lock(row_id)  # BLOCKIERT bis transaction1 fertig ist!

Problem: Bei hoher ParallelitΓ€t fΓΌhrt dies zu:

  • Warteschlangen und Bottlenecks
  • Deadlocks zwischen Transaktionen
  • Timeouts und Failed Transactions

Die MVCC-LΓΆsung

MVCC (Multi-Version Concurrency Control) erstellt stattdessen Versionen:

# MVCC: Optimistic Concurrency
transaction1.begin(snapshot_id=100)
# Liest Version 100 der Daten
transaction1.read(row_id)  # Version 100

# Gleichzeitig kann transaction2 lesen!
transaction2.begin(snapshot_id=100)
transaction2.read(row_id)  # Auch Version 100, keine Wartezeit!

# Transaction 1 schreibt
transaction1.update(row_id, value_a)  # Erstellt Version 101
transaction1.commit()  # Version 101 wird permanent

# Transaction 2 schreibt auch
transaction2.update(row_id, value_b)  # Will auch Version 101 erstellen
transaction2.commit()  # FEHLER: Conflict Detection!
# "Row was modified by another transaction"

Vorteil: Reads blockieren nie Writes, Writes blockieren nie Reads.

sequenceDiagram
    participant T1 as Transaction 1
    participant MVCC as MVCC Manager
    participant T2 as Transaction 2
    
    T1->>MVCC: BEGIN (snapshot_id=100)
    Note over T1,MVCC: T1 sieht Version 100
    
    T2->>MVCC: BEGIN (snapshot_id=100)
    Note over T2,MVCC: T2 sieht auch Version 100
    
    T1->>MVCC: READ(row_id)
    MVCC-->>T1: Version 100 (age=28)
    
    T2->>MVCC: READ(row_id)
    Note over T2,MVCC: Kein Warten!
    MVCC-->>T2: Version 100 (age=28)
    
    T1->>MVCC: UPDATE(row_id, age=29)
    Note over MVCC: Erstellt Version 101
    
    T1->>MVCC: COMMIT
    Note over MVCC: Version 101 wird permanent
    
    T2->>MVCC: UPDATE(row_id, age=30)
    T2->>MVCC: COMMIT
    Note over MVCC: [ERROR] Conflict Detection!
    MVCC--xT2: ERROR: Write-Write Conflict
Loading

Abb. 02.1: Systemarchitektur-Übersicht

Wie funktioniert MVCC intern?

Jede Datenzeile hat nicht nur einen Wert, sondern eine Historie:

Row ID: users/alice

Version 100 (committed):
  name: "Alice Smith"
  age: 28

Version 101 (committed):
  name: "Alice Smith"
  age: 29

Version 102 (in_progress, transaction_id=555):
  name: "Alice Johnson"
  age: 29

Snapshot-Reads: Eine Transaktion mit snapshot_id=100 sieht nur Versionen ≀ 100, die committed sind.

Write-Write Conflicts: Wenn zwei Transaktionen die gleiche Row Γ€ndern wollen, gewinnt die erste. Die zweite bekommt einen Conflict Error beim Commit.

graph LR
    subgraph "MVCC Version History"
        V100[Version 100<br/>age: 28<br/>status: committed]
        V101[Version 101<br/>age: 29<br/>status: committed]
        V102[Version 102<br/>age: 30<br/>status: in_progress<br/>tx_id: 555]
        
        V100 --> V101
        V101 --> V102
    end
    
    T1[Transaction 1<br/>snapshot_id=100] -.reads.-> V100
    T2[Transaction 2<br/>snapshot_id=101] -.reads.-> V101
    T3[Transaction 3<br/>snapshot_id=102] -.reads.-> V102
    
    style V100 fill:#95e1d3
    style V101 fill:#78e08f
    style V102 fill:#ffd32a
Loading

Abb. 02.2: Query-Engine-Komponenten


2.3 Transaction Management

ACID-Garantien

ThemisDB garantiert volle ACID-Properties:

A - Atomicity (AtomaritΓ€t):
Alle Operationen in einer Transaktion passieren komplett oder gar nicht.

# Beispiel: Geldtransfer
transaction.begin()
transaction.update("accounts/alice", {"balance": 900})  # -100
transaction.update("accounts/bob", {"balance": 1100})   # +100
transaction.commit()  # Beide Updates oder keines!

Wenn der Server zwischen den beiden update() Calls abstΓΌrzt:

  • Ohne AtomaritΓ€t: Alice verliert 100€, Bob bekommt nichts (Katastrophe!)
  • Mit AtomaritΓ€t: Beide Updates werden zurΓΌckgerollt (Konsistent!)

C - Consistency (Konsistenz):
Constraints werden immer eingehalten.

# Foreign Key Constraint
transaction.insert("orders", {
    "order_id": "o123",
    "user_id": "alice",  # Muss in users existieren
    "amount": 50.0
})
# Wenn user "alice" nicht existiert -> FEHLER!

I - Isolation (Isolierung):
Transaktionen sehen sich nicht gegenseitig.

# Transaction 1 liest
t1.read("products/laptop")  # price: 999

# Transaction 2 Γ€ndert (aber committed noch nicht)
t2.update("products/laptop", {"price": 899})

# Transaction 1 liest nochmal
t1.read("products/laptop")  # Immer noch 999!
# Sieht die Γ„nderung von T2 NICHT, bis T2 committet

D - Durability (Dauerhaftigkeit):
Einmal committed, sind Daten permanent - auch bei Serverausfall.

transaction.commit()
# Ab hier garantiert: Daten sind auf Disk!
# Selbst wenn Server JETZT abstΓΌrzt, sind Daten da.
stateDiagram-v2
    [*] --> Begin: transaction.begin()
    
    Begin --> Active: Snapshot erstellt
    
    Active --> Reading: read()
    Active --> Writing: write()
    Active --> Validating: commit() aufgerufen
    
    Reading --> Active
    Writing --> Active
    
    Validating --> CheckConflicts: PrΓΌfe Write-Write Conflicts
    
    CheckConflicts --> WriteWAL: Keine Konflikte
    CheckConflicts --> Rollback: Konflikt erkannt
    
    WriteWAL --> ApplyChanges: WAL persistent
    ApplyChanges --> Committed: Γ„nderungen sichtbar
    
    Committed --> [*]
    
    Active --> Rollback: rollback() oder Fehler
    Rollback --> [*]
    
    note right of WriteWAL
        Write-Ahead Log (WAL)
        garantiert Durability
    end note
    
    note right of CheckConflicts
        MVCC Conflict Detection:
        Hat andere Transaktion
        bereits committed?
    end note
Loading

Abb. 02.3: Storage-Layer-Struktur

Isolation Levels

ThemisDB implementiert Snapshot Isolation, ein Mittelweg zwischen Serializable und Read Committed:

Isolation Level Dirty Reads Non-Repeatable Reads Phantom Reads
Read Uncommitted βœ— MΓΆglich βœ— MΓΆglich βœ— MΓΆglich
Read Committed βœ“ Verhindert βœ— MΓΆglich βœ— MΓΆglich
Snapshot Isolation βœ“ Verhindert βœ“ Verhindert βœ“ Verhindert
Serializable βœ“ Verhindert βœ“ Verhindert βœ“ Verhindert

Snapshot Isolation ist performanter als Serializable, verhindert aber trotzdem alle Anomalien, die in der Praxis relevant sind.


2.4 Storage Layer: RocksDB

Warum RocksDB?

ThemisDB verwendet RocksDB als Storage-Engine. Diese Entscheidung basiert auf mehreren Faktoren:

1. LSM-Trees fΓΌr SSDs optimiert

Traditionelle B-Trees schreiben oft:

Write 1 byte β†’ Read 4KB page β†’ Modify 1 byte β†’ Write 4KB page

LSM-Trees (Log-Structured Merge Trees) schreiben sequentiell:

Write 1 byte β†’ Append to log β†’ Done
Later: Merge logs in background

Resultat: 10x schneller auf SSDs!

2. Battle-Tested bei Facebook

RocksDB verarbeitet bei Facebook:

  • Billions of operations per day
  • Petabytes of data
  • 10+ years Production Experience

3. Flexible Key-Value API

RocksDB ist ein Key-Value Store - perfekt als Foundation fΓΌr hΓΆhere Modelle:

// Relational: key = "users/alice"
db->Put("users/alice", json_data);

// Graph: key = "edges/from_alice_to_bob"
db->Put("edges/from_alice_to_bob", edge_data);

// Vector: key = "vectors/product_123"
db->Put("vectors/product_123", embedding_data);

Column Families

RocksDB unterstΓΌtzt "Column Families" - separate Namespaces innerhalb einer DB:

// Trennung nach Datenmodell
db->Put(cf_relational, "users/alice", data);
db->Put(cf_graph_edges, "alice->bob", edge);
db->Put(cf_vectors, "prod_123", embedding);

Vorteil: Jede Column Family kann eigene Optimierungen haben:

  • Relational: Starke Compression
  • Graph: Weniger Compression, mehr Cache
  • Vectors: Spezielle Bloom Filters

Das Base Entity Paradigma: Einheitlicher Multi-Modell-Speicher

ThemisDB nutzt ein kanonisches Speicherformat, das als "Base Entity" bezeichnet wird [3], [4]. Jede logische EntitΓ€t – sei es eine relationale Zeile, ein Graph-Knoten, ein Vektor-Objekt oder ein Dokument – wird als ein einziges binΓ€r-serialisiertes Dokument (als "Blob" bezeichnet) gespeichert [11].

Diese Architekturentscheidung ist fundamental fΓΌr die Multi-Modell-FΓ€higkeit von ThemisDB:

Multi-Modell-Datenabbildung auf physischer Ebene:

Logisches Modell Physischer Speicher Key-Format Value-Format
Relational (PK, Blob) "table_name:pk_value" VelocyPack/Bincode
Dokument (PK, Blob) "collection:pk_value" VelocyPack/Bincode
Graph (Knoten) (PK, Blob) "node:pk_value" VelocyPack/Bincode
Graph (Kante) (PK, Blob) "edge:pk_value" VelocyPack (inkl. _from/_to)
Vektor (PK, Blob) "object:pk_value" VelocyPack (inkl. Vektor-Array)

Wie funktioniert das Base Entity Pattern?

Um das Base Entity Paradigma zu verstehen, betrachten wir ein konkretes Beispiel: Stellen Sie sich vor, Sie speichern einen Benutzer mit dem Namen "Alice", der 30 Jahre alt ist und in Berlin wohnt.

Im traditionellen Ansatz (z.B. PostgreSQL):

-- Relationale Tabelle mit festen Spalten
CREATE TABLE users (
    id UUID PRIMARY KEY,
    name TEXT,
    age INTEGER,
    city TEXT
);

INSERT INTO users VALUES ('uuid-123', 'Alice', 30, 'Berlin');

Im ThemisDB Base Entity Ansatz:

ZunΓ€chst wird das logische Objekt in ein einheitliches BinΓ€rformat serialisiert:

// 1. Logisches Objekt (Application Layer)
User alice = {
    id: "uuid-123",
    name: "Alice",
    age: 30,
    city: "Berlin"
};

// 2. Serialisierung zu VelocyPack (binΓ€res JSON-Γ€hnliches Format)
// VelocyPack ist optimiert fΓΌr schnelles Parsing und kompakte Speicherung
std::vector<uint8_t> blob = VelocyPack::serialize(alice);
// Resultat: ~40 Bytes binΓ€re Daten statt ~80 Bytes ASCII-JSON

// 3. Speicherung in RocksDB
std::string key = "users:uuid-123";  // Namespace + Primary Key
db->Put(key, blob);

Dieser Blob ist das "Base Entity" – die kanonische Speicherform fΓΌr alle Datenmodelle. Entscheidend: Egal ob Sie einen relationalen Datensatz, einen Graph-Knoten, ein Dokument oder einen Vektor speichern – physisch ist es immer ein Key-Value-Paar mit binΓ€rem Blob als Value.

Wie werden unterschiedliche Datenmodelle auf Base Entities abgebildet?

  1. Relational (Tabellenzeile):

    // Key: "table_name:primary_key"
    // Value: BinΓ€r-serialisierte Zeile mit allen Spalten
    Key: "users:uuid-123"
    Value: VelocyPack({id, name, age, city})
  2. Graph-Knoten:

    // Key: "node:node_id"
    // Value: Knoten-Eigenschaften + Metadaten
    Key: "nodes:alice"
    Value: VelocyPack({_id, label: "Person", properties: {name, age}})
  3. Graph-Kante:

    // Key: "edge:edge_id"
    // Value: Kante mit _from, _to, Gewicht, Eigenschaften
    Key: "edges:knows-123"
    Value: VelocyPack({_from: "alice", _to: "bob", _weight: 1.0, since: "2020"})
  4. Vektor-Objekt:

    // Key: "vector:object_id"
    // Value: Objekt-Metadaten + Embedding-Array
    Key: "vectors:doc-456"
    Value: VelocyPack({id, metadata: {...}, embedding: [0.1, 0.2, ..., 0.9]})

Warum ist das ein Durchbruch?

Problem bei Polyglot Persistence: In traditionellen Multi-Modell-AnsΓ€tzen (z.B. PostgreSQL + Neo4j + ChromaDB) mΓΌssen Sie denselben Datenpunkt in mehreren Datenbanken speichern:

  • PostgreSQL: Metadaten (Name, Alter)
  • Neo4j: Graph-Beziehungen
  • ChromaDB: Vektor-Embedding

Resultat: Datenkonsistenz ist unmΓΆglich zu garantieren, weil atomare Transaktionen ΓΌber drei separate Systeme nicht mΓΆglich sind.

LΓΆsung mit Base Entity: Alle Informationen (Metadaten, Graph-Kontext, Vektor-Embedding) sind im selben Blob. Eine RocksDB-Transaktion kann atomar:

  • Den Base Entity-Blob aktualisieren
  • SekundΓ€rindizes aktualisieren (relationaler Index, Graph-Adjazenz, HNSW-Vector-Index)
  • Alles oder nichts (ACID)

Vorteile dieses Ansatzes:

  1. Einheitliche Speicherschicht: Alle Datenmodelle teilen sich denselben physischen Speicher [11]
  2. ACID ΓΌber alle Modelle: Transaktionen kΓΆnnen atomar ΓΌber Graph, Vector und Relational operieren [20]
  3. Effiziente Serialisierung: BinΓ€rformate wie VelocyPack [41] sind 4x schneller als Standard-JSON-Parser [40]

RocksDB TransactionDB: ACID-Garantien

ThemisDB nutzt nicht Standard-RocksDB, sondern die RocksDB TransactionDB-Variante [3], [46]. Diese bietet:

  1. Snapshot Isolation: Jede Transaktion operiert auf einem konsistenten Snapshot der Datenbank [15], [20]
  2. Conflict Detection: Parallele Transaktionen, die dieselben SchlΓΌssel bearbeiten, werden erkannt [46]
  3. Atomare Rollbacks: Fehlschlagende Transaktionen werden vollstΓ€ndig zurΓΌckgerollt [16]

Dies ist entscheidend: Die Aktualisierung einer einzelnen logischen EntitΓ€t (z.B. UPDATE users SET age = 31) erfordert die atomare Γ„nderung mehrerer physischer Key-Value-Paare:

  • Der "Base Entity"-Blob muss aktualisiert werden
  • SekundΓ€rindex-EintrΓ€ge mΓΌssen geΓ€ndert werden (z.B. LΓΆschen von idx:age:30, EinfΓΌgen von idx:age:31)

Ohne TransactionDB wΓ€re diese Konsistenz zwischen Base Entity-Blobs und Index-Projektionen nicht garantiert.

Write-Ahead Log (WAL)

Jede Write-Operation wird erst in ein Log geschrieben:

1. Client sendet: UPDATE users/alice SET age=29
2. WAL Write: "UPDATE users/alice age=29" β†’ Disk (sync!)
3. MemTable Update: In-Memory Update
4. Return OK to Client
...spΓ€ter...
5. Background Compaction: MemTable β†’ SSTable auf Disk

Crash Recovery: Wenn Server abstΓΌrzt:

  • MemTable (RAM) ist verloren
  • WAL (Disk) ist da
  • Beim Restart: WAL replay β†’ MemTable rebuild β†’ Normal operations

Wie funktioniert der WAL-Mechanismus im Detail?

Der Write-Ahead Log ist das HerzstΓΌck der Dauerhaftigkeit (Durability) in ThemisDB. Lassen Sie uns den Ablauf Schritt fΓΌr Schritt nachvollziehen:

Schritt 1 - Client sendet Write-Operation: Ein Client mΓΆchte den Benutzer "Alice" aktualisieren. Die Operation wird an den ThemisDB-Server gesendet:

client.update("users", "uuid-123", {age: 31});

Schritt 2 - WAL Write (kritisch fΓΌr Durability): Bevor irgendetwas im Hauptspeicher geΓ€ndert wird, schreibt ThemisDB die Operation in das Write-Ahead Log auf der Festplatte. Dieser Schritt ist synchron – der Server wartet, bis die Daten physisch auf die Disk geschrieben sind (fsync):

// Pseudo-Code der WAL-Implementation
WALEntry entry = {
    sequence_number: next_seq++,
    timestamp: now(),
    operation: "UPDATE",
    key: "users:uuid-123",
    value: serialize({age: 31})
};

// Kritisch: sync=true erzwingt fsync() - Daten MÜSSEN auf Disk sein
wal_file.append(entry, sync=true);  
// Erst wenn fsync() zurΓΌckkehrt, sind Daten dauerhaft gespeichert

Warum ist sync=true wichtig? Ohne fsync() wΓΌrden Daten nur im Betriebssystem-Cache liegen. Bei einem Stromausfall wΓ€ren sie verloren. Mit fsync() garantiert das OS, dass Daten auf physischen Plattern (oder SSD) sind.

Schritt 3 - MemTable Update (In-Memory): Erst nachdem der WAL-Eintrag sicher auf Disk ist, wird die Γ„nderung im MemTable (RAM-Struktur) vorgenommen:

// MemTable ist eine sortierte In-Memory-Map (z.B. Skip List)
memtable.put("users:uuid-123", {age: 31});

Das MemTable ist schnell (O(log n) fΓΌr Inserts), aber flΓΌchtig. Bei einem Crash ist es weg.

Schritt 4 - Return OK zum Client: Jetzt erst antwortet der Server dem Client mit "SUCCESS". Der Client hat die Garantie:

  • Daten sind dauerhaft (WAL auf Disk)
  • Selbst bei sofortigem Crash sind Daten nicht verloren

Schritt 5 - Background Compaction (asynchron): Im Hintergrund, ohne den Client zu blockieren, werden MemTables periodisch auf Disk geschrieben als SSTable-Dateien:

// Wenn MemTable voll ist (z.B. 256 MB)
if (memtable.size() > 256MB) {
    SSTable* sstable = memtable.flush_to_disk();
    // SSTable ist eine immutable, sortierte Datei auf Disk
    // Format: [Key1, Value1, Key2, Value2, ...]
}

Crash Recovery - Warum WAL lebensrettend ist:

Szenario: Server crashed nach Schritt 4, aber vor Schritt 5. Das MemTable (RAM) ist verloren, aber der WAL (Disk) ist da.

Beim Server-Restart:

// 1. WAL lesen und replay
for (entry in wal_file) {
    memtable.put(entry.key, entry.value);
}
// β†’ MemTable ist rekonstruiert!

// 2. Normale Operationen fortsetzen
server.start();

Resultat: Kein Datenverlust. Alle committed Transaktionen sind wiederhergestellt.

Performance-Trade-off: Der fsync() in Schritt 2 kostet Zeit (~1-5ms auf NVMe, ~10-20ms auf HDD). Deshalb ist die WAL-Platzierung auf schnellem NVMe-Speicher kritisch fΓΌr Write-Performance.

LSM-Tree Performance-Charakteristiken

Die Entscheidung fΓΌr eine LSM-Tree-Architektur [12] hat spezifische Performance-Implikationen:

Schreiboptimierung (Create/Update/Delete):

  • LSM-Trees sind inhΓ€rent schreiboptimiert [12], [14]
  • Jede C/U/D-Operation ist ein extrem schneller, sequentieller "Append-Only"-Vorgang in eine In-Memory-Struktur (das Memtable)
  • Benchmarks zeigen ca. 45.000 Writes pro Sekunde Durchsatz [3], [5]
  • Ideal fΓΌr die Ingestion-Pipeline (Covina) mit hohem Schreibdurchsatz

Lese-Performance-Optimierung:

  • Ein Punktabruf ΓΌber den PrimΓ€rschlΓΌssel (Get(PK)) ist schnell
  • Attribut-basierte Abfragen (z.B. SELECT * WHERE age > 30) wΓΌrden ohne Indizes einen Full-Scan aller Blobs erfordern [4]
  • Dies erzwingt architektonisch die Notwendigkeit der "Layer" (SekundΓ€rindizes) fΓΌr optimale Leseleistung [3]

Speicherhierarchie: ThemisDB implementiert eine ausgefeilte Speicherhierarchie [5]:

  • Heiße Daten: Residieren im Block Cache (RAM, standardmÀßig 1 GB) und in den oberen Levels des LSM-Trees (L0-L5)
  • Kompression: Obere Levels mit schnellem LZ4-Algorithmus [37] komprimiert (33,8 MB/s Throughput)
  • Kalte Daten: Wandern in das unterste Level (L6) mit ZSTD-Kompression [38] fΓΌr maximale Speicherdichte (2,8x Ratio)
  • Automatische Optimierung: Background Compaction verschiebt Daten zwischen Levels [12]

Speicherhierarchie: Von VRAM bis HDD

Die Performance von ThemisDB hΓ€ngt entscheidend davon ab, wo Daten gespeichert werden. Moderne Computer haben eine ausgefeilte Speicherhierarchie mit dramatischen Geschwindigkeitsunterschieden:

Die Speicherpyramide (von schnell nach langsam):

                    ╔═══════════════════╗
                    β•‘  CPU L1 Cache     β•‘  < 1 ns   (4-64 KB)
                    ╠═══════════════════╣
                    β•‘  CPU L2 Cache     β•‘  ~3 ns    (256 KB - 1 MB)
                    ╠═══════════════════╣
                    β•‘  CPU L3 Cache     β•‘  ~10 ns   (8-64 MB, shared)
                    ╠═══════════════════╣
                    β•‘  RAM (DDR4/DDR5)  β•‘  ~100 ns  (16-512 GB)
                    ╠═══════════════════╣
           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β•¨β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β•¨β”€β”€β”€β”€β”€β”€β”€β”€β”
           β”‚       NUMA Boundary (~200-300ns)     β”‚
           β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                    ╠═══════════════════╣
                    β•‘  NVMe SSD (PCIe4) β•‘  ~10 ΞΌs   (1-8 TB)
                    ╠═══════════════════╣
                    β•‘  SATA SSD         β•‘  ~50 ΞΌs   (256 GB - 4 TB)
                    ╠═══════════════════╣
                    β•‘  HDD (7200 RPM)   β•‘  ~5 ms    (4-20 TB)
                    β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Faktor zwischen schnellstem und langsamstem: ~5.000.000x (!)

Wie nutzt ThemisDB diese Hierarchie optimal?

1. Heiße Daten im RAM (Block Cache):

ThemisDB konfiguriert RocksDB mit einem großen Block Cache (standardmÀßig 1-4 GB, konfigurierbar bis 128 GB+):

// Aus docs/de/performance/performance_memory.md
RocksDBWrapper::Config config;
config.block_cache_size_mb = 4096;  // 4 GB Block Cache
config.cache_index_and_filter_blocks = true;
config.pin_l0_filter_and_index_blocks_in_cache = true;
config.high_pri_pool_ratio = 0.5;  // 50% fΓΌr Index/Filter

Was wird gecacht?

  • Data Blocks: Die eigentlichen Key-Value-Paare (50% des Cache)
  • Index Blocks: Index-Strukturen fΓΌr schnelles Suchen (25% des Cache, High-Priority)
  • Filter Blocks: Bloom-Filter zur Vermeidung unnΓΆtiger Disk-Reads (25% des Cache, High-Priority)

Wirkung: Ein Cache-Hit (Daten sind im RAM) hat ~100ns Latenz. Ein Cache-Miss (Disk-Read nΓΆtig) hat ~10ΞΌs Latenz auf NVMe. Faktor 100x schneller!

2. Write-Ahead Log auf schnellstem Medium (NVMe):

Das WAL ist write-kritisch. Jede Schreiboperation wartet auf einen fsync(). Deshalb sollte das WAL auf dem schnellsten verfΓΌgbaren Medium liegen:

// Separates WAL-Verzeichnis auf dedizierter NVMe
config.db_path = "/data/rocksdb";        // Haupt-DB (kann langsamer sein)
config.wal_dir = "/nvme/fast/wal";       // WAL auf schnellster NVMe

Performance-Gewinn:

  • NVMe (PCIe4): ~10ΞΌs fsync β†’ 45.000 Writes/Sekunde [3], [5]
  • SATA SSD: ~50ΞΌs fsync β†’ 20.000 Writes/Sekunde
  • HDD: ~5ms fsync β†’ 200 Writes/Sekunde

Faktor 225x zwischen NVMe und HDD!

3. LSM-Tree Levels mit gestufter Kompression:

ThemisDB nutzt unterschiedliche Kompression fΓΌr verschiedene LSM-Tree Levels:

config.compression_default = "lz4";       // Level 0-5 (heiß)
config.compression_bottommost = "zstd";   // Level 6+ (kalt)

Warum?

  • L0-L5 (heiße Daten): Werden hΓ€ufig gelesen β†’ LZ4 (33.8 MB/s Throughput [5], 2-3x Kompression)
  • L6 (kalte Daten): Werden selten gelesen β†’ ZSTD (2.8x Kompression [5], aber langsamer)

Speicher-Trade-off visualisiert:

Level 0 (RAM): MemTable            β†’ 256 MB, unkomprimiert
               ↓ flush
Level 1 (NVMe): Junge SSTables     β†’ ~512 MB, LZ4-komprimiert
               ↓ compaction
Level 2 (NVMe): Γ„ltere SSTables    β†’ ~2 GB, LZ4-komprimiert
               ↓ compaction
Level 3-5 (NVMe/SATA): Alte Daten  β†’ ~20 GB, LZ4-komprimiert
               ↓ compaction
Level 6 (SATA/HDD): Kalte Daten    β†’ ~200 GB, ZSTD-komprimiert (max. Dichte)

Automatische Datenmigration:

ThemisDB verschiebt Daten automatisch "nach unten":

  1. Neue Writes β†’ MemTable (RAM, ultra-schnell)
  2. MemTable voll β†’ Flush zu L1 (NVMe, schnell)
  3. Compaction β†’ Daten wandern zu L2, L3, ... (progressiv langsamer)
  4. Kalte Daten β†’ L6 (maximal komprimiert, platzsparend)

Resultat: HΓ€ufig zugegriffene Daten bleiben "oben" (schnell), selten genutzte Daten wandern "nach unten" (langsam aber platzsparend).

4. GPU-VRAM fΓΌr HNSW-Index (optional, fΓΌr Vektor-Suche):

Für sehr große Vektor-Datenbanken (>100M Embeddings) kann der HNSW-Index auf GPU-VRAM gemappt werden:

// GPU-Beschleunigung fΓΌr Vektor-Γ„hnlichkeitssuche
HNSWConfig hnsw_config;
hnsw_config.use_gpu = true;
hnsw_config.gpu_device = 0;  // CUDA device 0
// VRAM: ~1-5 ns Latenz, aber begrenzte Grâße (8-80 GB)

Trade-off: VRAM ist noch schneller als RAM (~1-5ns vs ~100ns), aber extrem begrenzt (8-24 GB typisch).

Zusammenfassung: Speicherhierarchie-Strategie:

Datentyp Optimal Platziert Latenz BegrΓΌndung
MemTable RAM ~100 ns Aktive Writes, Γ€ndert sich stΓ€ndig
WAL NVMe (PCIe4) ~10 ΞΌs Write-kritisch, sync-Operationen
Block Cache RAM ~100 ns HΓ€ufig gelesene BlΓΆcke
L0-L5 SSTables NVMe ~10 μs Heiße Daten, hÀufig gelesen
L6 SSTables SATA SSD/HDD ~50 ΞΌs - 5 ms Kalte Daten, selten gelesen
HNSW-Index RAM (oder GPU-VRAM) ~100 ns (~5ns GPU) Vektor-Suche, latenz-kritisch
Backups HDD/Tape/S3 Sekunden Langzeit-Archivierung

Best Practice fΓΌr Production:

# NVMe PCIe4 fΓΌr WAL und L0-L3
/nvme/fast/ β†’ 2 TB NVMe PCIe4 fΓΌr WAL + Hot SSTables

# SATA SSD fΓΌr L4-L6
/ssd/bulk/ β†’ 8 TB SATA SSD fΓΌr Cold SSTables

# HDD fΓΌr Backups
/backup/ β†’ 40 TB HDD Array fΓΌr Point-in-Time Snapshots

2.5 Praxis: Todo-App

Jetzt bauen wir eine vollstΓ€ndige Todo-App, um die Architektur in Aktion zu sehen. Basis ist examples/02_todo_app.

Architektur der Todo-App

Die Todo-App folgt dem MVC-Pattern und demonstriert alle ACID-Eigenschaften:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     UI (Tkinter)                        β”‚
β”‚  β€’ Task List (Treeview)                 β”‚
β”‚  β€’ Detail Panel (Form)                  β”‚
β”‚  β€’ Toolbar (Buttons)                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Controller (main.py)                β”‚
β”‚  β€’ Event Handlers                       β”‚
β”‚  β€’ Business Logic                       β”‚
β”‚  β€’ Validation                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     TodoClient (themis_client.py)       β”‚
β”‚  β€’ CRUD Operations                      β”‚
β”‚  β€’ Transaction Management               β”‚
β”‚  β€’ Error Handling                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     ThemisDB Server                     β”‚
β”‚  β€’ MVCC Transactions                    β”‚
β”‚  β€’ Secondary Indexes                    β”‚
β”‚  β€’ ACID Guarantees                      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Datenmodell

FΓΌr unsere Todo-App definieren wir zuerst das Datenmodell mit Python Dataclasses. Wir nutzen Enumerations fΓΌr Status und PrioritΓ€t, um Type-Safety zu gewΓ€hrleisten und Tippfehler zu vermeiden. Die Klasse Task kapselt alle Informationen eines Tasks und bietet Methoden zur Serialisierung fΓΌr ThemisDB.

πŸ“ VollstΓ€ndiger Code: examples/02_todo_app/models.py

# Kernkomponenten des Task-Modells (Auszug)

from dataclasses import dataclass
from datetime import datetime
from enum import Enum

class TaskStatus(Enum):
    OPEN = "open"
    IN_PROGRESS = "in_progress"
    DONE = "done"

class TaskPriority(Enum):
    LOW = "low"
    NORMAL = "normal"
    HIGH = "high"

@dataclass
class Task:
    id: str
    title: str
    status: TaskStatus
    priority: TaskPriority
    created_at: datetime
    updated_at: datetime
    # ... weitere Felder siehe vollstΓ€ndige Datei
    
    def to_dict(self):
        """Serialisierung fΓΌr ThemisDB - konvertiert Task zu Dictionary"""
        return {
            "_key": self.id,
            "title": self.title,
            "status": self.status.value,
            "priority": self.priority.value,
            "created_at": self.created_at.isoformat(),
            # ... vollstΓ€ndige Implementierung in models.py
        }

Wichtige Design-Entscheidungen:

  1. Enums fΓΌr Status/Priority: Verhindert ungΓΌltige Werte und bietet Type-Safety
  2. Dataclass-Decorator: Generiert automatisch __init__, __repr__, __eq__ Methoden
  3. to_dict/from_dict Methoden: Klare Trennung von Serialisierungslogik
  4. Type Hints: ErmΓΆglicht IDE-UnterstΓΌtzung und frΓΌhzeitige Fehlererkennung

Setup und Installation

# ThemisDB starten (Docker)
docker run -d -p 8765:8765 themisdb/themisdb:1.3.4

# Todo-App installieren
cd examples/02_todo_app
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate
pip install -r requirements.txt

Client-Implementierung

Der TodoClient kapselt alle Datenbankoperationen und bietet eine saubere API fΓΌr unsere Anwendung. Die Klasse initialisiert beim Start automatisch die benΓΆtigte Collection und erstellt Performance-Indizes auf hΓ€ufig gefilterten Feldern wie Status und PrioritΓ€t. Dies demonstriert Best Practices fΓΌr die Arbeit mit ThemisDB.

πŸ“ VollstΓ€ndiger Code: examples/02_todo_app/themis_client.py

KernfunktionalitΓ€t - Database Setup:

class TodoClient:
    def __init__(self, host="localhost", port=8765):
        self.client = Client(host, port)
        self._setup_database()
    
    def _setup_database(self):
        """Erstellt Collection und Indizes fΓΌr optimale Performance"""
        self.client.create_collection("tasks", type="document")
        
        # Performance-Indizes auf oft gefilterten Feldern
        self.client.create_index("tasks", "status_idx", ["status"])
        self.client.create_index("tasks", "priority_idx", ["priority"])
        self.client.create_index("tasks", "due_date_idx", ["due_date"])

CRUD-Operationen (Auszug):

    def create_task(self, task: Task) -> bool:
        """Erstellt einen neuen Task in der Datenbank"""
        self.client.insert("tasks", task.to_dict())
        return True
    
    def update_task(self, task: Task) -> bool:
        """Aktualisiert Task - MVCC erkennt Konflikte automatisch!"""
        try:
            self.client.update("tasks", task.id, task.to_dict())
            return True
        except ConflictError:
            print("Task was modified by another user!")
            return False

Query-Beispiele mit AQL:

    def list_tasks(self, status: Optional[TaskStatus] = None) -> List[Task]:
        """Demonstriert AQL-Queries mit dynamischen Filtern"""
        query = "FOR task IN tasks"
        
        if status:
            query += f' FILTER task.status == "{status.value}"'
        
        query += " SORT task.created_at DESC RETURN task"
        
        results = self.client.query(query)
        return [Task.from_dict(data) for data in results]

Die vollstΓ€ndige Implementierung enthΓ€lt zusΓ€tzlich get_task(), delete_task() und search_tasks() Methoden. Siehe vollstΓ€ndige Datei fΓΌr alle Details.

MVCC und Transaktionen demonstriert

Die Todo-App zeigt MVCC in Aktion:

Szenario: Zwei Benutzer Γ€ndern gleichzeitig den gleichen Task

# User 1: Startet Transaction
user1 = TodoClient()
task = user1.get_task("task_123")  # Snapshot Version 100
task.status = TaskStatus.IN_PROGRESS
task.updated_at = datetime.now()

# User 2: Auch Transaction, gleicher Task
user2 = TodoClient()
task2 = user2.get_task("task_123")  # Auch Snapshot Version 100
task2.priority = TaskPriority.HIGH
task2.updated_at = datetime.now()

# User 1 committed zuerst
user1.update_task(task)  # βœ“ SUCCESS, Version 101

# User 2 versucht zu committen
user2.update_task(task2)  # βœ— CONFLICT!
# Error: "Task was modified by another transaction"

Was passiert intern?

  1. Beide lesen Version 100 (Snapshot Isolation)
  2. User 1 schreibt Version 101 und committed
  3. User 2 versucht auch Version 101 zu schreiben
  4. ThemisDB erkennt: "Version 101 existiert bereits!"
  5. Conflict Error wird geworfen

LΓΆsung im Code:

def update_task_with_retry(self, task: Task, max_retries=3):
    """Update mit automatischem Retry bei Conflicts"""
    for attempt in range(max_retries):
        try:
            self.client.update("tasks", task.id, task.to_dict())
            return True
        except ConflictError:
            # Re-read aktuellste Version
            latest = self.get_task(task.id)
            if not latest:
                return False
            
            # Merge changes (Application Logic)
            # z.B.: Keep newer updated_at
            if latest.updated_at > task.updated_at:
                task = latest
            
            # Retry mit gemergten Daten
            continue
        except Exception as e:
            return False
    
    return False  # Max retries exceeded

2.6 Index-Strukturen

Warum Indizes?

Ohne Index: Full Table Scan

-- Ohne Index: O(n)
SELECT * FROM tasks WHERE status = 'open'
-- Muss ALLE Tasks durchgehen: 10.000 Tasks = 10.000 Reads

Mit Index: Direkt zum Ergebnis

-- Mit Index auf status: O(log n + k), k = Anzahl Results
SELECT * FROM tasks WHERE status = 'open'
-- B-Tree Lookup: log(10.000) β‰ˆ 13 Reads + nur relevante Tasks

Index-Typen in ThemisDB

1. B-Tree Index (Default)

client.create_index("tasks", "status_idx", ["status"])
  • Sortierte Daten
  • Range Queries: WHERE age > 18 AND age < 65
  • Equality: WHERE status = 'open'

2. Hash Index

client.create_index("tasks", "id_hash_idx", ["id"], type="hash")
  • Nur Equality: WHERE id = 'task_123'
  • Schneller als B-Tree fΓΌr Equality
  • Keine Range Queries

3. Geo Index (Hilbert Curves)

client.create_index("locations", "geo_idx", ["coordinates"], type="geo")
  • RΓ€umliche Queries
  • Nearest Neighbor
  • Bounding Box Searches

4. Fulltext Index

client.create_index("tasks", "fulltext_idx", ["title", "description"], type="fulltext")
  • Tokenization
  • Stemming
  • Relevance Scoring

5. Vector Index (HNSW)

client.create_index("products", "embedding_idx", ["embedding"], type="vector", dimensions=768)
  • Approximate Nearest Neighbor
  • Cosine Similarity
  • Euclidean Distance

Index-Performance

Operation Ohne Index Mit B-Tree Mit Hash
Equality (=) O(n) O(log n) O(1)
Range (>, <) O(n) O(log n + k) βœ—
Sort O(n log n) O(k) βœ—

k = Anzahl Ergebnisse


2.7 Zusammenfassung

In diesem Kapitel haben Sie gelernt:

βœ… Schichten-Architektur: 5 Schichten mit klaren Verantwortlichkeiten
βœ… MVCC: ParallelitΓ€t ohne Locks durch Versionierung
βœ… Transaktionen: ACID-Garantien und Snapshot Isolation
βœ… RocksDB: LSM-Trees, WAL, Column Families
βœ… Indizes: B-Tree, Hash, Geo, Fulltext, Vector
βœ… Todo-App: VollstΓ€ndige CRUD-Anwendung mit MVCC

Was macht ThemisDB besonders?

  1. Multi-Model MVCC: Transaktionen ΓΌber alle Modelle hinweg
  2. RocksDB Foundation: Battle-tested, SSD-optimiert
  3. Flexible Indizes: FΓΌr jeden Use Case der richtige Index
  4. Snapshot Isolation: Performance + Konsistenz

NΓ€chste Schritte

Im nΓ€chsten Kapitel lernen Sie, wie die verschiedenen Datenmodelle kombiniert werden kΓΆnnen und wann welches Modell am besten passt.

Kapitel 3: Multi-Model verstehen β†’


WeiterfΓΌhrende Ressourcen


2.11 BaseEntity: Die Speichereinheit

ThemisDB speichert alle Daten - unabhΓ€ngig vom Modell (relational, document, graph, vector) - als BaseEntity. Dies ist die fundamentale Speichereinheit.

2.11.1 BaseEntity Architektur

class BaseEntity {
public:
    using Blob = std::vector<uint8_t>;
    using FieldMap = std::map<std::string, Value>;
    
    enum class Format { BINARY, JSON };
    
    // PrimΓ€rschlΓΌssel
    const std::string& getPrimaryKey() const;
    void setPrimaryKey(std::string_view pk);
    
    // Feld-Zugriff (lazy parsing)
    std::optional<Value> getField(std::string_view field_name) const;
    void setField(std::string_view field_name, const Value& value);
    
    // Serialisierung
    Blob serialize() const;
    std::string toJson() const;
    
    // Factory-Methoden
    static BaseEntity fromJson(std::string_view pk, std::string_view json_str);
    static BaseEntity fromFields(std::string_view pk, const FieldMap& fields);
    static BaseEntity deserialize(std::string_view pk, const Blob& blob);
    
private:
    std::string primary_key_;
    Blob blob_;
    Format format_ = Format::BINARY;
    mutable std::shared_ptr<FieldMap> field_cache_;
};

Kernkonzept: Jede EntitΓ€t ist ein einzelnes binΓ€res Blob mit effizienter Serialisierung und Lazy-Parsing fΓΌr schnellen Feldzugriff.

2.11.2 Value-Typsystem

ThemisDB verwendet ein typsicheres std::variant-System:

Typ C++ Typ Verwendung Beispiel
null std::monostate Fehlende/leere Werte NULL
bool bool Wahrheitswerte true, false
int int64_t Ganzzahlen 42, -1000
double double Gleitkommazahlen 3.14, 1e10
string std::string Texte "Hello World"
vector std::vector<float> Embeddings fΓΌr ANN [0.1, 0.2, 0.3]
binary std::vector<uint8_t> BinΓ€rdaten Bilder, PDFs

Wichtig: Nested Objects/Arrays sind aktuell nicht unterstΓΌtzt (geplant fΓΌr v1.4).

2.11.3 BaseEntity Lifecycle

flowchart TB
    A[JSON Input] --> B[fromJson]
    B --> C[BaseEntity in Memory]
    C --> D[serialize]
    D --> E[Binary Blob]
    E --> F[RocksDB Storage]
    
    F --> G[Read from RocksDB]
    G --> H[deserialize]
    H --> C
    
    C --> I[getField lazy parsing]
    I --> J[field_cache_]
    
    C --> K[toJson]
    K --> L[JSON Output]
    
    style C fill:#e1f5ff
    style J fill:#fff4e1
    style F fill:#ffe1e1
Loading

Abb. 02.4: Transaction-Management-Flow

Lazy Parsing: Felder werden erst geparst, wenn sie angefordert werden - spart CPU-Zeit bei großen Objekten.


2.12 Key Schema: Multi-Modell-Namespacing

ThemisDB vereint alle Datenmodelle in einer einzigen RocksDB-Instanz durch ein hierarchisches SchlΓΌsselsystem mit PrΓ€fixen.

2.12.1 SchlΓΌsselformate

Datenmodell Format Beispiel Beschreibung
Relational entity:table:pk entity:users:alice Tabellenzeilen
Document entity:collection:pk entity:orders:order_123 Dokumente
Graph Node entity:node:pk entity:node:user_456 Graph-Knoten
Graph Edge entity:edge:pk entity:edge:follows_789 Graph-Kanten
Vector entity:vectors:pk entity:vectors:doc_abc Vektor-Embeddings
Secondary Index idx:table:column:value:pk idx:users:age:30:alice Equality Indexes
Range Index ridx:table:column:value:pk ridx:products:price:99.99:prod_1 Range Queries
Spatial Index sidx:table:geohash:pk sidx:locations:u33dc1:berlin Geo-Queries
TTL Index ttlidx:table:column:ts:pk ttlidx:sessions:created:1730000000:s1 Time-based Expiry
Fulltext ftidx:table:column:token:pk ftidx:articles:body:search:art_42 Volltextsuche
Graph Outdex graph:out:pk_start:pk_edge graph:out:alice:follows_789 Ausgehende Kanten
Graph Indeg graph:in:pk_target:pk_edge graph:in:bob:follows_789 Eingehende Kanten
Changefeed changefeed:pk:seqno changefeed:alice:0000000001 CDC Stream
Time-Series ts:metric:entity:ts ts:cpu:server1:1730000000000 Metriken

Separator: Alle SchlΓΌssel verwenden : als Trennzeichen fΓΌr schnelles Prefix-Scanning.

2.12.2 Key Schema Visualisierung

flowchart LR
    A[RocksDB] --> B[entity: Prefix]
    A --> C[idx: Prefix]
    A --> D[graph: Prefix]
    A --> E[ts: Prefix]
    
    B --> B1[entity:users:alice]
    B --> B2[entity:orders:order_123]
    B --> B3[entity:node:user_456]
    
    C --> C1[idx:users:age:30:alice]
    C --> C2[ridx:products:price:99.99:prod_1]
    C --> C3[ftidx:articles:body:search:art_42]
    
    D --> D1[graph:out:alice:follows_789]
    D --> D2[graph:in:bob:follows_789]
    
    E --> E1[ts:cpu:server1:1730000000000]
    E --> E2[ts:memory:server1:1730000000000]
    
    style A fill:#e1f5ff
    style B fill:#fff4e1
    style C fill:#e1ffe1
    style D fill:#ffe1f5
    style E fill:#f5e1ff
Loading

Abb. 02.5: MVCC-Versionskontrolle

2.12.3 Primary Key Extraktion

// KeySchema API
std::string pk = KeySchema::extractPrimaryKey("idx:users:age:30:alice");
// Ergebnis: "alice"

KeyType type = KeySchema::parseKeyType("entity:users:alice");
// Ergebnis: KeyType::RELATIONAL

std::string key = KeySchema::makeRelationalKey("users", "alice");
// Ergebnis: "entity:users:alice"

Vorteil: Durch PrΓ€fixe kΓΆnnen alle Indizes einer Tabelle oder alle Kanten eines Knotens mit einem einzigen RocksDB-Scan gelesen werden.


2.13 MVCC Deep-Dive: Implementierungsdetails

Nachdem wir in Abschnitt 2.2 die Grundlagen von MVCC kennengelernt haben, tauchen wir nun in die Implementierungsdetails ein.

2.13.1 RocksDB TransactionDB Architektur

flowchart TB
    A[Application] --> B[TransactionManager]
    B --> C[TransactionWrapper]
    
    C --> D[RocksDB TransactionDB]
    D --> E[Snapshot Manager]
    D --> F[Lock Manager]
    D --> G[WAL]
    
    E --> H[Versioned Data]
    F --> I[Write-Write Conflict Detection]
    G --> J[Durability]
    
    subgraph "MVCC Engine"
    D
    E
    F
    G
    end
    
    style B fill:#e1f5ff
    style D fill:#fff4e1
    style E fill:#e1ffe1
    style F fill:#ffe1e1
Loading

Abb. 02.6: Index-Strukturen

2.13.2 Snapshot Isolation Flow

// Transaction A liest Snapshot zum Zeitpunkt t=100
auto txn_a = db.beginTransaction();
auto snapshot = txn_a->getSnapshot();  // t=100

// Transaction B schreibt zur gleichen Zeit
auto txn_b = db.beginTransaction();
txn_b->put("entity:users:alice", "{age: 31}");
txn_b->commit();  // Commit zur Zeit t=101

// Transaction A sieht alte Version (t=100)
auto value = txn_a->get("entity:users:alice");
// value = "{age: 30}" (alte Version)

txn_a->commit();

2.13.3 Write-Write Conflict Detection

sequenceDiagram
    participant T1 as Transaction 1
    participant T2 as Transaction 2
    participant DB as RocksDB
    
    T1->>DB: get(users:alice)
    DB-->>T1: {age: 30}
    
    T2->>DB: get(users:alice)
    DB-->>T2: {age: 30}
    
    T1->>DB: put(users:alice, {age: 31})
    Note over T1,DB: Acquires Lock
    
    T2->>DB: put(users:alice, {age: 32})
    Note over T2,DB: Lock Conflict Detected!
    
    T1->>DB: commit()
    DB-->>T1: OK (age=31 persisted)
    
    T2->>DB: commit()
    DB-->>T2: CONFLICT (rollback)
Loading

Abb. 02.7: Replication-Topologie

KonfliktauflΓΆsung: Transaction 2 wird automatisch zurΓΌckgerollt - die Anwendung muss die Transaktion wiederholen.

2.13.4 MVCC Performance

Benchmark-Ergebnisse (v1.5.0-dev):

Operation MVCC (ops/s) WriteBatch (ops/s) Overhead
Single Entity Insert 3,400 3,100 +9.7%
Batch Insert (100) 27,800 27,800 0%
Snapshot Read 44,000 - Baseline
Rollback 35,300 - Baseline

Fazit: MVCC-Overhead ist minimal bei Batch-Operationen - Snapshot-Isolation kostet fast nichts!

2.13.5 Index-Konsistenz mit MVCC

Problem: Wenn eine Transaktion zurΓΌckgerollt wird, mΓΌssen alle Indizes konsistent bleiben.

LΓΆsung: Alle Index-Operationen sind Teil der Transaktion:

// Atomare Transaktion: Entity + Secondary Index + Graph Outdex
auto txn = db.beginTransaction();

// 1. Insert Entity
txn->put(KeySchema::makeRelationalKey("users", "alice"), 
         entity.serialize());

// 2. Update Secondary Index
txn->put(KeySchema::makeSecondaryIndexKey("users", "age", "30", "alice"),
         empty_value);

// 3. Update Graph Outdex
txn->put(KeySchema::makeGraphOutdexKey("alice", "follows_789"),
         edge_data);

// Alles oder nichts
if (!txn->commit()) {
    // Conflict detected - alle 3 Operationen werden zurΓΌckgerollt
}

Garantie: Entweder werden alle Indizes aktualisiert oder keiner - keine Inkonsistenzen mΓΆglich!


2.14 Compression Strategy

ThemisDB verwendet differenzierte Kompression je nach Datentyp fΓΌr optimale Balance zwischen Speicherersparnis und Performance.

2.14.1 Kompressionsarten

Datentyp Compression Ratio CPU-Overhead Use Case
Time-Series Gorilla 10-20x +15% Metriken, Sensordaten
Vektoren SQ8 Quantization 4x +20% Embeddings ab 1M Vektoren
Content Blobs ZSTD Level 19 1.5-2x +30% Dokumente, Bilder
JSON Metadata LZ4 (RocksDB) 2-3x +5% Structured Data
Graph Edges LZ4 (RocksDB) 2x +5% Adjacency Lists

2.14.2 Gorilla Time-Series Compression

Gorilla ist ein hocheffizienter Codec fΓΌr Time-Series-Daten von Facebook:

// Gorilla Encoding
std::vector<uint8_t> GorillaCodec::encode(
    const std::vector<int64_t>& timestamps,
    const std::vector<double>& values
) {
    // Delta-of-Delta Encoding fΓΌr Timestamps
    // XOR-Encoding fΓΌr Float-Werte
    // Typical Ratio: 12-20x
}

Beispiel:

Raw Data (100k Punkte):
- Timestamps: 800 KB (8 bytes Γ— 100k)
- Values: 800 KB (8 bytes Γ— 100k)
- Total: 1.6 MB

Gorilla Compressed:
- Total: 80-130 KB
- Ratio: 12-20x

2.14.3 Vector Quantization (SQ8)

Scalar Quantization reduziert float32 (4 bytes) auf int8 (1 byte):

flowchart LR
    A[Float32 Vector] --> B[Compute Min/Max]
    B --> C[Scale to 0-255]
    C --> D[int8 Vector]
    D --> E[Store with Scale Params]
    
    E --> F[Dequantize on Search]
    F --> G[HNSW Search]
    
    style A fill:#e1f5ff
    style D fill:#e1ffe1
    style G fill:#fff4e1
Loading

Abb. 02.8: Failover-Mechanismus

Trade-off:

  • βœ… 4x Speicherersparnis
  • βœ… Bessere Cache-Nutzung β†’ schnellere Suche
  • ⚠️ 95-98% Recall@10 (minimal schlechter als float32)

2.14.4 RocksDB Block Compression

ThemisDB konfiguriert RocksDB mit Level-based Compression:

// Level 0-5: LZ4 (schnell fΓΌr Hot Data)
options.compression_per_level[0] = rocksdb::kLZ4Compression;
options.compression_per_level[1] = rocksdb::kLZ4Compression;
// ...

// Level 6+: Zstandard (hΓΆhere Ratio fΓΌr Cold Data)
options.compression_per_level[6] = rocksdb::kZSTD;
options.compression_per_level[7] = rocksdb::kZSTD;

Rationale: Hot Data (Level 0-1) benΓΆtigt schnellen Zugriff β†’ LZ4. Cold Data (Level 6+) profitiert von hΓΆherer Kompression β†’ Zstandard.


2.15 Zusammenfassung Architektur

In diesem erweiterten Kapitel haben Sie die Tiefe der ThemisDB-Architektur kennengelernt:

Kernkonzepte

  1. BaseEntity: Unified Storage Layer fΓΌr alle Datenmodelle
  2. Key Schema: Hierarchisches Namespacing mit PrΓ€fixen
  3. MVCC: Snapshot Isolation ohne Locks
  4. Compression: Differenzierte Strategien (Gorilla, SQ8, ZSTD)
  5. Index-Konsistenz: Atomare Transaktionen ΓΌber alle Indizes

Architektur-Prinzipien

  • Separation of Concerns: Klar getrennte Schichten
  • Performance First: Lazy Parsing, Lazy Indexing
  • Multi-Model Unity: Ein Speicher, viele Modelle
  • Concurrency Without Locks: MVCC statt Pessimistic Locking
  • Adaptive Compression: Datentyp-spezifische Algorithmen

Weitere Ressourcen


2.16 Phase-3-Sync: WeiterfΓΌhrende Referenzen (docs/de/) {#chapter02_16_cross-references}

Detaillierte Implementierungsdokumentation zu den behandelten Architektur-Themen:

Thema Referenz (docs/de/)
Architektur-Überblick docs/de/architecture/ARCHITECTURE_OVERVIEW.md
DIP-Architektur (VollstΓ€ndig) docs/de/architecture/COMPLETE_DIP_ARCHITECTURE.md
DMS Moderne Architektur docs/de/architecture/DMS_MODERN_ARCHITECTURE.md
Modularisierungs-Entscheidung docs/de/architecture/MODULARIZATION_DECISION.de.md
Modularisierungsplan docs/de/architecture/MODULARIZATION_PLAN.md
Binary Protocol Buffers docs/de/architecture/BINARY_PROTOCOL_BUFFERS.md
DLL Signaturverifizierung docs/de/architecture/DLL_SIGNATURE_VERIFICATION.md
Linux Module Signing docs/de/architecture/LINUX_MODULE_SIGNING.md

β†’ ZurΓΌck: Kapitel 1: EinfΓΌhrung
β†’ Weiter: Kapitel 3: Installation


Kapitel 2 von 30 | Teil I: Grundlagen | ~14.000 WΓΆrter | Phase-3-Sync: βœ…


ThemisDB 1.9.0-beta Β· Home Β· Module-Index Β· GitHub Β· Issues

ThemisDB Wiki

🏠 Overview

πŸ“š Compendium

πŸš€ Getting Started

πŸ“– Tutorials

πŸ“— User Guide

βš™οΈ Operations & Security

πŸ“Ÿ Ops Runbooks

πŸ—οΈ Architecture

πŸ“ ADRs

πŸ”§ Contributing

πŸ“‹ Governance

πŸ” Audit

🧩 Plugins

πŸ”Œ Adapters

πŸ’‘ Examples

πŸ“¦ Client SDKs

πŸŽ“ Training

πŸ› οΈ Tools

πŸ€– Developer LLM Wiki

Clone this wiki locally