Eventuele consistentie: strategieën, afwegingen en UX-patronen
Een gebruiker werkt zijn profiel bij, klikt op 'Opslaan' en ziet nog steeds zijn oude naam. Herlaad de pagina: nu zie je de nieuwe naam. In het midden: er stond een gebeurtenisgestuurd systeem het doorgeven van de update tussen services. Dit is deuiteindelijke consistentie in actie – en het verkeerde antwoord is het antwoord dat de gebruiker denkt dat het systeem heeft een bug. Het juiste antwoord, dat van de architect, is om het systeem zo te ontwerpen dit venster van inconsistentie is onzichtbaar of acceptabel.
Eventuele consistentie is geen compromis dat verborgen moet blijven: het is een architecturale keuze bewust met echte voordelen (beschikbaarheid, schaalbaarheid, veerkracht) en echte uitdagingen (complexiteit, testen, UX). Begrijp consistentiepatronen en strategieën voor management Tijdelijke inconsistenties zijn essentieel voor het bouwen van gedistribueerde systemen die werken echt in productie.
Wat je gaat leren
- CAP-stelling en BASE versus ACID: het theoretische raamwerk
- Consistentiepatronen: sterk tot uiteindelijk
- Read-Your-Writes: de strategie om oude gegevens niet onmiddellijk na het schrijven weer te geven
- Optimistische gebruikersinterface: werkt de interface bij voordat de server reageert
- Conflictdetectie en afstemming voor gelijktijdig gewijzigde gegevens
- Versiebeheer met vectorklokken om afwijkingen te detecteren
- Hoe u de mogelijke consistentie aan de gebruiker kunt communiceren zonder hem in verwarring te brengen
CAP-stelling en BASE: het theoretische raamwerk
Il CAP-stelling stelt dat in een gedistribueerd systeem met partities netwerk (P onvermijdelijk in productie), je kunt beide hebben Caanhoudendheid (alle knooppunten zien dezelfde gegevens) o Abeschikbaarheid (het systeem reageert altijd), maar niet beide tegelijk tijdens een partitie.
| Eigendom | ZUUR (relationele DB) | BASIC (gedistribueerde systemen) |
|---|---|---|
| Samenhang | Sterk: elke keer dat je leest, zie je het laatste dat je schrijft | Uiteindelijk: knooppunten convergeren in de loop van de tijd |
| Beschikbaarheid | Niet gegarandeerd tijdens partities | Hoog: het systeem reageert altijd |
| Verblijf | Atomair: alles of niets | Zachte toestand: de toestand kan veranderen zonder input |
| Voorbeelden | PostgreSQL, MySQL, Oracle | DynamoDB, Cassandra, CouchDB |
Consistentiemodellen: het spectrum
Tussen sterke consistentie en uiteindelijke consistentie bestaan veel tussenmodellen. Als u ze kent, kunt u voor elke gebruikssituatie de juiste afweging maken:
// Spettro dei modelli di consistenza (dal piu forte al piu debole)
// 1. STRONG (Linearizable) Consistency
// Ogni operazione sembra atomica e globalmente ordinata
// Esempio: PostgreSQL con una singola istanza
// Trade-off: latenza alta, disponibilita ridotta
// 2. SEQUENTIAL Consistency
// Le operazioni appaiono nell'ordine in cui vengono eseguite
// ma non necessariamente in tempo reale
// Esempio: spanner.google.com (TrueTime)
// 3. CAUSAL Consistency
// Le operazioni causalmente correlate sono viste nell'ordine corretto
// Le operazioni non correlate possono essere viste in ordine diverso
// Esempio: MongoDB con causally consistent sessions
// 4. READ-YOUR-WRITES (Session Consistency)
// Un client vede sempre le sue ultime scritture
// Altri client potrebbero vedere dati vecchi
// Esempio: DynamoDB con sticky sessions
// 5. MONOTONIC READ Consistency
// Una volta letto un valore, non vedrai mai un valore piu vecchio
// Non garantisce di vedere le proprie ultime scritture
// 6. EVENTUAL Consistency
// I nodi convergono allo stesso stato "eventualmente"
// Nessuna garanzia su quando o nell'ordine delle operazioni
// Esempio: DNS, DynamoDB default, AWS S3
Read-Your-Writes: toon oude gegevens niet onmiddellijk na het schrijven
Het meest voorkomende probleem met uiteindelijke consistentie in de productie: de gebruiker werkt iets bij, wordt doorgestuurd naar de detailpagina en ziet het opnieuw de oude waarde omdat de replica de wijziging nog niet heeft doorgevoerd. Lees-uw-schrijf-consistentie zorgt ervoor dat een klant ziet altijd uw laatste geschriften.
// Strategia 1: Sticky routing — leggi sempre dallo stesso nodo
// Il client viene indirizzato sempre alla replica primaria per
// un periodo dopo la scrittura (es. 1-5 secondi)
// Implementazione con un token di versione
interface WriteResult {
entityId: string;
version: number; // numero di versione dopo la scrittura
timestamp: number; // timestamp della scrittura
}
// Il client salva il WriteResult e lo invia nelle successive letture
interface ReadRequest {
entityId: string;
minVersion?: number; // "voglio almeno questa versione"
}
// Il server: attendi che la replica raggiunga la versione richiesta
class ConsistentReadService {
async readWithVersion(
entityId: string,
minVersion?: number,
timeoutMs = 5000
): Promise<Entity> {
const startTime = Date.now();
while (true) {
const entity = await this.replica.findById(entityId);
if (!minVersion || entity.version >= minVersion) {
return entity;
}
if (Date.now() - startTime > timeoutMs) {
// Timeout: leggi dal primario come fallback
return await this.primary.findById(entityId);
}
await this.sleep(50); // Breve attesa prima del retry
}
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
// Strategia 2: Redirect al primario per N secondi dopo scrittura
// Il CDN/LB indirizza le letture del client alla primary replica
// per 2-3 secondi dopo una scrittura
// Strategia 3: Cache invalidation
// Dopo la scrittura, invalida la cache del client
// La prossima lettura va direttamente alla primary
Optimistische gebruikersinterface: eerst bijwerken, later afstemmen
L'Optimistische gebruikersinterface en het fundamentele UX-patroon voor systemen tot uiteindelijke consistentie: update de interface onmiddellijk (ervan uitgaande dat dat het schrijven succesvol zal zijn), wordt vervolgens afgestemd met de server. Maakt de systeem wordt als onmiddellijk ervaren, zelfs met een hoge netwerklatentie.
// Optimistic UI con React e reconciliazione
import { useState, useOptimistic } from 'react';
interface Todo {
id: string;
text: string;
completed: boolean;
synced: boolean; // false = in attesa di conferma server
}
function TodoList({ initialTodos }: { initialTodos: Todo[] }) {
const [todos, setTodos] = useState(initialTodos);
// useOptimistic: hook React 19 per Optimistic UI
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state: Todo[], newTodo: Todo) => [...state, newTodo]
);
async function handleToggle(todoId: string): Promise<void> {
const todo = todos.find((t) => t.id === todoId);
if (!todo) return;
const optimisticUpdate = { ...todo, completed: !todo.completed, synced: false };
// 1. Aggiorna UI immediatamente (ottimistico)
addOptimisticTodo(optimisticUpdate);
try {
// 2. Invia al server
const confirmed = await api.toggleTodo(todoId);
// 3. Sostituisci con il dato confermato dal server
setTodos((prev) =>
prev.map((t) => (t.id === todoId ? { ...confirmed, synced: true } : t))
);
} catch (error) {
// 4. ROLLBACK: ripristina lo stato originale se la scrittura fallisce
setTodos((prev) => prev.map((t) => (t.id === todoId ? todo : t)));
// Mostra feedback all'utente
showErrorToast('Impossibile aggiornare il task. Riprova.');
}
}
return (
<ul>
{optimisticTodos.map((todo) => (
<li key={todo.id} className={todo.synced ? '' : 'pending'}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => handleToggle(todo.id)}
/>
{todo.text}
{!todo.synced && <span className="sync-indicator">Salvataggio...</span>}
</li>
))}
</ul>
);
}
Conflictdetectie met vectorklokken
Wanneer twee gebruikers gelijktijdig dezelfde gegevens op systemen wijzigen bij meervoudige replicatie ontstaat er een conflict. DE Vectorklokken kunt u automatisch afwijkingen detecteren en beslissen wat u gaat doen een automatische samenvoeging of presenteer het conflict aan de gebruiker.
// Vector Clock: implementazione base in TypeScript
type VectorClock = Record<string, number>;
function increment(clock: VectorClock, nodeId: string): VectorClock {
return { ...clock, [nodeId]: (clock[nodeId] ?? 0) + 1 };
}
function merge(a: VectorClock, b: VectorClock): VectorClock {
const result: VectorClock = { ...a };
for (const [node, time] of Object.entries(b)) {
result[node] = Math.max(result[node] ?? 0, time);
}
return result;
}
type CompareResult = 'before' | 'after' | 'concurrent' | 'equal';
function compare(a: VectorClock, b: VectorClock): CompareResult {
const allNodes = new Set([...Object.keys(a), ...Object.keys(b)]);
let aGreater = false;
let bGreater = false;
for (const node of allNodes) {
const aTime = a[node] ?? 0;
const bTime = b[node] ?? 0;
if (aTime > bTime) aGreater = true;
if (bTime > aTime) bGreater = true;
}
if (aGreater && bGreater) return 'concurrent'; // conflitto
if (aGreater) return 'after';
if (bGreater) return 'before';
return 'equal';
}
// Utilizzo per rilevare conflitti di scrittura concorrente
interface Document {
id: string;
content: string;
vectorClock: VectorClock;
lastModifiedBy: string;
}
async function updateDocument(
docId: string,
newContent: string,
clientClock: VectorClock,
nodeId: string
): Promise<{ success: boolean; conflict?: { local: Document; remote: Document } }> {
const current = await db.findById(docId);
const comparison = compare(clientClock, current.vectorClock);
if (comparison === 'concurrent') {
// Conflitto! Entrambe le versioni hanno avanzato indipendentemente
return {
success: false,
conflict: {
local: { ...current, content: newContent, vectorClock: clientClock },
remote: current,
},
};
}
if (comparison === 'before') {
// Il client ha una versione stale: rifiuta e richiedi re-fetch
throw new Error('Stale write: fetch the latest version first');
}
// OK: scrivi con clock aggiornato
const newClock = increment(merge(clientClock, current.vectorClock), nodeId);
await db.update(docId, newContent, newClock, nodeId);
return { success: true };
}
Verzoeningsstrategieën
Wanneer u een conflict ontdekt, beschikt u over verschillende verzoeningsstrategieën: elk geschikt voor verschillende gegevenstypen:
// Strategie di reconciliazione dei conflitti
// 1. Last-Write-Wins (LWW)
// Il timestamp piu recente vince. Semplice ma perde dati.
// Usato da: AWS DynamoDB (default), Apache Cassandra
function resolveWithLWW(a: Document, b: Document): Document {
return a.updatedAt > b.updatedAt ? a : b;
}
// 2. Merge automatico per strutture dati CRDT-friendly
// Counter: somma i delta (non i valori assoluti)
interface Counter {
nodeIncrements: Record<string, number>; // per ogni nodo: totale incrementi
}
function mergeCounters(a: Counter, b: Counter): Counter {
const result: Record<string, number> = { ...a.nodeIncrements };
for (const [node, count] of Object.entries(b.nodeIncrements)) {
result[node] = Math.max(result[node] ?? 0, count);
}
return { nodeIncrements: result };
}
function getCounterValue(counter: Counter): number {
return Object.values(counter.nodeIncrements).reduce((sum, v) => sum + v, 0);
}
// 3. Three-way merge (come Git)
// Confronta le due versioni divergenti con il loro antenato comune
function threeWayMerge(
ancestor: string,
versionA: string,
versionB: string
): { merged: string; hasConflicts: boolean } {
// Usa diff3 o similar per testi
// Per strutture dati: merge field-by-field
// Se un campo e stato modificato in A ma non in B: accetta la modifica di A
// Se un campo e stato modificato in entrambi: conflitto da risolvere manualmente
// Implementazione completa dipende dal tipo di dato
return { merged: versionA, hasConflicts: true }; // placeholder
}
// 4. User-driven conflict resolution
// Presenta entrambe le versioni all'utente e lascia scegliere
async function presentConflictToUser(
conflict: { local: Document; remote: Document }
): Promise<Document> {
const resolution = await showConflictDialog({
localVersion: conflict.local,
remoteVersion: conflict.remote,
message: 'Il documento e stato modificato da un altro utente. Quale versione vuoi mantenere?',
});
return resolution.chosen === 'local' ? conflict.local : conflict.remote;
}
UX-patronen voor uiteindelijke consistentie
Het meest onderschatte onderdeel van uiteindelijke consistentie is de communicatie met de gebruiker. Gebruikers begrijpen consistentiemodellen niet (en zouden dat ook niet moeten begrijpen), maar ze nemen het wel waar onmiddellijk als iets niet werkt zoals ze verwachten.
Aanbevolen UX-patronen
- Verouderde indicatoren: toont "2 minuten geleden bijgewerkt" in plaats van de latentie te verbergen. Gebruikers begrijpen dat de gegevens mogelijk niet de nieuwste zijn.
- Visueel in afwachting van status: gebruik spinners, grijze indicatoren of de tekst "Opslaan..." om aan te geven wanneer een bewerking wordt uitgevoerd.
- Succesbevestiging: toont alleen een bevestigingstoast/banner na bevestiging van de server, niet optimistisch voor kritieke bewerkingen.
- Automatisch opnieuw proberen: voor niet-kritieke bewerkingen kunt u het op de achtergrond opnieuw proberen. Geef de fout alleen weer als alle nieuwe pogingen mislukken.
- Zacht verwijderen vóór hard verwijderen: toon het item onmiddellijk als verwijderd, maar voer de daadwerkelijke verwijdering pas uit na bevestiging van de server.
// Pattern: Stale-While-Revalidate per dati non critici
async function useStaleWhileRevalidate<T>(
key: string,
fetcher: () => Promise<T>
): Promise<{ data: T; isStale: boolean }> {
// 1. Servi immediatamente i dati in cache (potenzialmente stale)
const cached = await cache.get<{ data: T; timestamp: number }>(key);
if (cached) {
const isStale = Date.now() - cached.timestamp > STALE_THRESHOLD_MS;
if (isStale) {
// 2. Avvia revalidation in background (non bloccante)
fetcher()
.then((freshData) => {
cache.set(key, { data: freshData, timestamp: Date.now() });
})
.catch(console.error);
}
return { data: cached.data, isStale };
}
// 3. Nessuna cache: fetch bloccante
const freshData = await fetcher();
await cache.set(key, { data: freshData, timestamp: Date.now() });
return { data: freshData, isStale: false };
}
// Utilizzo nel frontend:
// const { data: userProfile, isStale } = await useStaleWhileRevalidate(
// `profile:${userId}`,
// () => api.getUserProfile(userId)
// );
// if (isStale) showBanner('Dati potenzialmente non aggiornati');
Afweging om te overwegen
Niet alle gegevens tolereren uiteindelijke consistentie. Productprijzen, verkoop van lopende rekeningen, de beschikbaarheid van vliegtickets: deze operaties vereisen sterke consistentie. Eventuele consistentie is geschikt voor sociale feeds en tellers van likes, analytische gegevens, gebruikersprofielen, niet-kritieke inventarissen. Identificeer expliciet welke gegevens welk niveau van consistentie in uw architectuur vereisen.
Conclusies: Uiteindelijke consistentie als een bewuste keuze
Uiteindelijke consistentie is geen zwakte van gedistribueerde systemen: het is een keuze doelbewust dat de schaalbaarheid, beschikbaarheid en veerkracht van ACID-systemen mogelijk maakt ze kunnen niet bieden. De sleutel is om het systeem zo te ontwerpen dat inconsistenties voorkomen tijdelijk zijn onzichtbaar voor de gebruiker (optimistische gebruikersinterface, Read-Your-Writes) of expliciet gecommuniceerd wanneer dit onvermijdelijk is.
Dit artikel sluit de serie Event-Driven Architecture af. Je hebt nu inzicht compleet met tools en patronen om betrouwbare gedistribueerde systemen te bouwen: van domeingebeurtenissen tot het Outbox-patroon, van DLQ tot consumenten-idempotence, tot aan de strategieën om elke consistentie in de productie te beheren.
Hele serie gebeurtenisgestuurde architectuur
- EDA Fundamentals: domeingebeurtenissen, opdrachten en berichtenbus
- Event Sourcing: Staat als een onveranderlijke opeenvolging van gebeurtenissen
- CQRS: gescheiden lezen en schrijven
- Saga-patroon: gedistribueerde transacties
- AWS EventBridge: Serverloze gebeurtenisbus
- Wachtrij met dode letters en veerkracht
- Idempotentie bij consumenten
- Outbox-patroon met CDC







