Consecvență eventuală: strategii, compromisuri și modele UX
Un utilizator își actualizează profilul, dă clic pe „Salvează” și își vede în continuare vechiul nume. Reîncărcați pagina: acum vedeți noul nume. În mijloc: un sistem bazat pe evenimente a stat propagarea actualizării între servicii. Acesta esteeventuala consistenta în acțiune — iar răspunsul greșit este cel pe care utilizatorul crede că are sistemul un bug. Răspunsul corect, cel al arhitectului, este de a proiecta sistemul astfel încât această fereastră de inconsecvență este invizibilă sau acceptabilă.
O posibilă consistență nu este un compromis de ascuns: este o alegere arhitecturală conștient cu beneficii reale (disponibilitate, scalabilitate, rezistență) și provocări reale (complexitate, testare, UX). Înțelegeți modelele de coerență și strategiile de gestionare Inconsecvențele temporare sunt esențiale pentru a construi sisteme distribuite care funcționează într-adevăr în producție.
Ce vei învăța
- Teorema CAP și BAZĂ vs ACID: cadrul teoretic
- Modele de consistență: puternic până la eventual
- Read-Your-Writes: strategia de a nu afișa datele vechi imediat după o scriere
- Interfață de utilizare optimistă: actualizează interfața înainte ca serverul să răspundă
- Detectarea conflictelor și reconcilierea datelor modificate concomitent
- Versiune cu ceasuri vectoriale pentru a detecta divergențele
- Cum să-i comunici utilizatorului posibila consistență fără a-l deruta
Teorema CAP și BAZĂ: Cadrul teoretic
Il Teorema CAP afirmă că într-un sistem distribuit cu partiții rețea (P inevitabil în producție), puteți avea oricare Consistență (toate nodurile văd aceleași date) o Adisponibilitate (sistemul răspunde întotdeauna), dar nu ambele în același timp în timpul unei partiții.
| Proprietate | ACID (DB relațional) | BASIC (sisteme distribuite) |
|---|---|---|
| Consecvență | Puternic: Fiecare citire vede ultima scriere | Eventual: Nodurile converg în timp |
| Disponibilitate | Nu este garantat în timpul partițiilor | Ridicat: sistemul răspunde întotdeauna |
| Şedere | Atomic: totul sau nimic | Stare soft: starea se poate schimba fără intrare |
| Exemple | PostgreSQL, MySQL, Oracle | DynamoDB, Cassandra, CouchDB |
Modele de consistență: Spectrul
Între consistența puternică și consistența eventuală există multe modele intermediare. Cunoașterea acestora vă permite să alegeți compromisul potrivit pentru fiecare caz de utilizare:
// 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
Citiți-vă scrierile: nu afișați datele vechi imediat după scriere
Cea mai frecventă problemă cu eventuala consistență în producție: utilizatorul actualizează ceva, este redirecționat către pagina de detalii și vede din nou valoarea veche deoarece replica nu a propagat încă modificarea. Consecvența Citește-ți-Scrierile se asigură că un client vede întotdeauna cele mai recente scrieri ale tale.
// 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
Interfață de utilizare optimistă: actualizați mai întâi, reconciliați mai târziu
L'Interfață de utilizare optimistă și modelul UX fundamental pentru sisteme la o eventuală consistență: actualizați imediat interfața (presupunând că scrierea va avea succes), apoi se reconciliază cu serverul. Face sistem perceput ca instantaneu chiar și cu o latență mare a rețelei.
// 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>
);
}
Detectarea conflictelor cu ceasuri vectoriale
Când doi utilizatori modifică aceleași date simultan pe sisteme cu replicare multiplă, se creează un conflict. THE Ceasuri vectoriale vă permit să detectați automat divergențele și să decideți ce să faceți o îmbinare automată sau prezenta conflictul utilizatorului.
// 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 };
}
Strategii de reconciliere
Când detectați un conflict, aveți la dispoziție mai multe strategii de reconciliere, fiecare adecvat pentru diferite tipuri de date:
// 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;
}
Modele UX pentru coerență eventuală
Cea mai subestimată parte a coerenței eventuale este comunicarea cu utilizatorul. Utilizatorii nu înțeleg (și nici nu ar trebui să înțeleagă) modelele de consistență, dar percep imediat când ceva nu funcționează așa cum se așteaptă.
Modele UX recomandate
- Indicatori vechi: afișează „Actualizat acum 2 minute” în loc să ascundă latența. Utilizatorii înțeleg că este posibil ca datele să nu fie cele mai proaspete.
- Vizual de stare în așteptare: utilizați rotitori, indicatori gri sau textul „Se salvează...” pentru a arăta când este în desfășurare o operațiune.
- Confirmare succes: afișează un toast/banner de confirmare numai după confirmarea serverului, nu în mod optimist pentru operațiuni critice.
- Reîncercați automat: pentru operațiuni non-critice, reîncercați silențios. Afișați eroarea numai dacă toate încercările eșuează.
- Soft Delete înainte de Hard Delete: afișați imediat elementul ca șters, dar ștergeți efectiv numai după confirmarea serverului.
// 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');
Schimb de luat în considerare
Nu toate datele tolerează o eventuală consistență. Prețuri produse, vânzări a conturilor curente, disponibilitatea biletelor de avion: aceste operațiuni necesită consistenta puternica. Consecvența eventuală este adecvată pentru fluxurile sociale, contoare de aprecieri, date de analiză, profiluri de utilizatori, inventare necritice. Identificați explicit ce date necesită ce nivel de consistență în arhitectura dvs.
Concluzii: Consecvența eventuală ca o alegere conștientă
Consecvența eventuală nu este o slăbiciune a sistemelor distribuite: este o alegere deliberat care permite scalabilitatea, disponibilitatea și rezistența sistemelor ACID nu pot oferi. Cheia este să proiectați sistemul astfel încât inconsecvențele temporare sunt invizibile pentru utilizator (Optimistic UI, Read-Your-Writes) sau comunicat în mod explicit atunci când este inevitabil.
Acest articol încheie seria Event-Driven Architecture. Acum ai o înțelegere complet cu instrumente și modele pentru a construi sisteme distribuite fiabile: de la evenimente de domeniu la modelul Outbox, de la DLQ la idempotenta consumatorului, până la strategiile de a gestiona orice consistență în producție.
Întreaga serie Arhitectură condusă de evenimente
- Fundamentele EDA: evenimente de domeniu, comenzi și magistrală de mesaje
- Aprovizionarea evenimentelor: starea ca o secvență imuabilă de evenimente
- CQRS: Citirea și scrierea separate
- Saga Pattern: Tranzacții distribuite
- AWS EventBridge: Autobuz de evenimente fără server
- Coadă de scrisori moarte și rezistență
- Idempotenta la consumatori
- Model de expediere cu CDC







