Případná konzistence: Strategie, kompromisy a vzory uživatelského rozhraní
Uživatel aktualizuje svůj profil, klikne na „Uložit“ a stále vidí své staré jméno. Znovu načtěte stránku: nyní uvidíte nový název. Uprostřed stál systém řízený událostmi šíření aktualizace mezi službami. Toto jepřípadná konzistence v akci – a špatná odpověď je ta, kterou si uživatel myslí, že systém má chyba. Správná odpověď architekta je navrhnout systém tak, aby toto okno nekonzistence je neviditelné nebo přijatelné.
Možná konzistence není kompromisem, který by se měl skrývat: je to architektonická volba vědomi si skutečných výhod (dostupnost, škálovatelnost, odolnost) a skutečných výzev (složitost, testování, UX). Porozumět vzorcům konzistence a strategiím řízení Dočasné nekonzistence jsou nezbytné pro vytváření fungujících distribuovaných systémů opravdu ve výrobě.
Co se naučíte
- Teorém CAP a BASE vs ACID: teoretický rámec
- Vzorce konzistence: silné až eventuální
- Read-Your-Writes: strategie nezobrazovat stará data ihned po zápisu
- Optimistické uživatelské rozhraní: Aktualizuje rozhraní, než server odpoví
- Detekce konfliktů a usmíření pro souběžně upravená data
- Verze s vektorovými hodinami pro detekci odchylek
- Jak sdělit uživateli možnou konzistenci, aniž by byl zmaten
CAP Věta a ZÁKLAD: Teoretický rámec
Il Věta CAP uvádí, že v distribuovaném systému s oddíly sítě (P nevyhnutelné ve výrobě), můžete mít buď Cstálost (všechny uzly vidí stejná data) o Adostupnost (systém vždy odpovídá), ale ne obojí současně během rozdělení.
| Vlastnictví | ACID (relační DB) | BASIC (distribuované systémy) |
|---|---|---|
| Konzistence | Silný: Každé čtení vidí poslední zápis | Případné: Uzly se v průběhu času sbíhají |
| Dostupnost | Během dělení není zaručeno | Vysoká: Systém vždy reaguje |
| Pobyt | Atomový: vše nebo nic | Měkký stav: Stav se může změnit bez zadání |
| Příklady | PostgreSQL, MySQL, Oracle | DynamoDB, Cassandra, CouchDB |
Modely konzistence: Spektrum
Mezi silnou konzistencí a případnou konzistencí existuje mnoho přechodných modelů. Jejich znalost vám umožňuje zvolit správný kompromis pro každý případ použití:
// 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: Nezobrazujte stará data ihned po zápisu
Nejčastější problém s případnou konzistencí ve výrobě: uživatel něco aktualizuje, bude přesměrován na stránku s podrobnostmi a zobrazí se znovu starou hodnotu, protože replika ještě nerozšířila změnu. Konzistence Read-Your-Writes zajišťuje, že klient vidí vždy vaše nejnovější spisy.
// 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
Optimistické uživatelské rozhraní: Nejprve aktualizujte, později sladíte
L'Optimistické uživatelské rozhraní a základní vzor UX pro systémy k případné konzistenci: okamžitě aktualizujte rozhraní (za předpokladu že zápis bude úspěšný), pak se odsouhlasí se serverem. Dělá systém vnímán jako okamžitý i při vysoké latenci sítě.
// 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>
);
}
Detekce konfliktů s vektorovými hodinami
Když dva uživatelé upravují stejná data v systémech současně při vícenásobné replikaci vzniká konflikt. THE Vektorové hodiny vám umožní automaticky detekovat odchylky a rozhodnout, co dělat automatické sloučení nebo zobrazení konfliktu uživateli.
// 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 };
}
Strategie usmíření
Když zjistíte konflikt, máte k dispozici několik strategií usmíření, každý vhodný pro různé typy dat:
// 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 vzory pro případnou konzistenci
Nejvíce podceňovanou částí případné konzistence je komunikace s uživatelem. Uživatelé nerozumí (ani by neměli rozumět) modelům konzistence, ale vnímají okamžitě, když něco nefunguje tak, jak očekávali.
Doporučené vzory UX
- Zastaralé indikátory: místo skrytí latence zobrazuje „Aktualizováno před 2 minutami“. Uživatelé chápou, že data nemusí být nejčerstvější.
- Vizuální stav čekající na vyřízení: pomocí číselníků, šedých indikátorů nebo textu „Ukládání...“ zobrazte probíhající operaci.
- Potvrzení úspěchu: zobrazuje potvrzovací toast/banner pouze po potvrzení serverem, ne optimisticky pro kritické operace.
- Automatické opakování: pro nekritické operace proveďte tiché opakování. Zobrazit chybu pouze v případě, že všechny pokusy selžou.
- Soft Delete před Hard Delete: okamžitě zobrazit položku jako odstraněnou, ale skutečné odstranění proveďte až po potvrzení serveru.
// 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');
Je třeba zvážit kompromis
Ne všechna data tolerují případnou konzistenci. Ceny produktů, prodej běžných účtů, dostupnost letenek: tyto operace vyžadují silná konzistence. Případná konzistence je vhodná pro sociální krmiva, pulty lajků, analytických údajů, uživatelských profilů, nekritických inventářů. Identifikovat explicitně, která data vyžadují jakou úroveň konzistence ve vaší architektuře.
Závěry: Případná konzistence jako vědomá volba
Případná konzistence není slabinou distribuovaných systémů: je to volba záměrné, které umožňuje škálovatelnost, dostupnost a odolnost, kterou mají systémy ACID nemohou nabídnout. Klíčem je navrhnout systém tak, aby nesrovnalosti dočasné jsou pro uživatele neviditelné (Optimistic UI, Read-Your-Writes) popř explicitně sděleno, když je to nevyhnutelné.
Tento článek uzavírá sérii Event-Driven Architecture. Nyní máte pochopení kompletní s nástroji a vzory pro vytváření spolehlivých distribuovaných systémů: od doménových událostí po vzor pošty k odeslání, od DLQ po idempotenci spotřebitelů, až po strategie řízení jakékoli konzistence ve výrobě.
Architektura řízená událostmi celé série
- Základy EDA: Doménové události, příkazy a sběrnice zpráv
- Sourcing událostí: Stav jako neměnná sekvence událostí
- CQRS: Samostatné čtení a zápis
- Vzor ságy: Distribuované transakce
- AWS EventBridge: Sběrnice událostí bez serveru
- Fronta mrtvého dopisu a odolnost
- Impotence u spotřebitelů
- Vzor k odeslání s CDC







