Modelos de Consistência: strong, eventual, causal, read-your-writes
- ⬜⚖️ CAP e PACELC: o teorema que define toda arquitetura distribuída(Sistemas Distribuídos)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
"Eventualmente consistente" e "fortemente consistente" são slogans que escondem uma hierarquia rica. Este módulo explica a escada de modelos — linearizability, sequential, causal, eventual — e as session guarantees(read-your-writes, monotonic reads, bounded staleness) que resolvem a maioria dos problemas reais sem pagar o custo de consistência global.
A escada de consistência (do mais forte ao mais fraco)
Strict / Linearizability ← mais forte, mais caro
│
▼
Sequential consistency
│
▼
Causal consistency
│
▼
PRAM / FIFO consistency
│
▼
Session guarantees ← prático e suficiente em 80%
(read-your-writes, monotonic r/w)
│
▼
Eventual consistency ← mais fraco, mais rápido
| Modelo | Garantia | Preço típico |
|---|---|---|
| Linearizability | Ordem global + tempo real; parece single-threaded | Quorum/consenso; latência alta |
| Sequential | Ordem total coerente com ordem de cada proc. | Menor que linearizability, mas ainda exige coordenação |
| Causal | Preserva happens-before; concorrentes livres | Vector clocks / dependency tracking |
| PRAM / FIFO | Cada escritor visto em ordem por todos | Muito barato, raramente suficiente |
| Session guarantees | Consistência dentro da sessão do cliente | Sticky session ou LSN tracking |
| Eventual | Sem novas escritas, converge | Mínimo; resolução LWW/CRDT/manual |
Linearizability: o modelo que parece 'um banco só'
Linearizability é o padrão mental default do desenvolvedor single-node: "o que escrevi agora, qualquer leitura depois vê". Em sistema distribuído isso exige coordenação — commit em quorum, ou consenso (Raft/Paxos) para decidir ordem global.
Operações de 3 clientes em paralelo: Client A: ──── write(x=1) ─── [ack] ───────────────────────────── Client B: ─────── read(x) ──────── [retorna 1] ────────────────── Client C: ─────────────── read(x) ─────── [retorna 1] ─────────── Linearizability exige: tudo parece ter ocorrido em UM instante entre início e fim de cada op, respeitando ordem real. Se B começou READ APÓS o ack de A, B DEVE ver x=1.
Causal consistency: preservando o 'porque'
Causal consistency (Lamport/Ahamad) define que toda ordem causal é respeitada por todos observadores. Se operação A happens-before B, ninguém vê B sem A. Operações concorrentes (sem causa-efeito) podem aparecer em qualquer ordem.
Cenário clássico de chat: Alice posta: "Acabei de me casar!" (evento e1) Bob responde: "Parabéns!" (evento e2, depende de e1) Carol abre o app e lê os dois. Causal exige que Carol veja e1 ANTES de e2. Eventual não garante — Carol poderia ver só "Parabéns!" até o post chegar. Linearizability garante, mas cobra ordem total global — caro demais. Vector clocks ou dependency tracking resolvem causal com custo razoável.
Session guarantees: o que resolve UX real
| Guarantee | O que garante | Quando importa |
|---|---|---|
| Read-your-writes | Depois de escrever, suas leituras refletem | Atualização de perfil, submit de form |
| Monotonic reads | Leituras sucessivas não "voltam no tempo" | Feed, lista paginada |
| Monotonic writes | Suas escritas aplicam em ordem | Updates sequenciais no mesmo recurso |
| Writes-follow-reads | Se você leu X, suas escritas vêm depois | Comentário depois de ler post |
| Bounded staleness | Leitura atrasa no máximo Δ tempo ou N versões | Dashboard, métricas |
# Read-your-writes em Postgres com read replicas — pattern com LSN no cookie
import psycopg
def write(conn, user_id: str, bio: str) -> int:
with conn.cursor() as cur:
cur.execute("UPDATE users SET bio=%s WHERE id=%s RETURNING pg_current_wal_lsn()", (bio, user_id))
lsn = cur.fetchone()[0]
conn.commit()
return lsn # envia para client (cookie/header)
def read(user_id: str, client_lsn: str | None) -> dict:
# Se o client tem LSN, obrigue leitura em replica com replay >= LSN
if client_lsn:
replica = pick_replica_at_or_after(client_lsn) # sua função de routing
if not replica:
replica = PRIMARY # fallback
else:
replica = pick_any_replica()
with replica.cursor() as cur:
cur.execute("SELECT bio FROM users WHERE id=%s", (user_id,))
return {"bio": cur.fetchone()[0]}Bounded staleness: o contrato do 'quase tempo real'
Em dashboards, relatórios, alimentação de BI e feature stores, você aceita atraso — desde que limitado. Bounded staleness define o limite: "leitura atrasa no máximo 5 segundos ou 100 versões". Sistemas como Azure Cosmos DB expõem essa opção como nível de consistência.
| Caso | Modelo adequado | Por que |
|---|---|---|
| Transferência bancária | Linearizability | Dinheiro não divergente |
| Leaderboard de jogo | Bounded staleness (~5s) | Pequeno atraso é ok; ranking não precisa ser exato |
| Feed social | Causal | Ordem reply-to importa; ordem global não |
| Carrinho de compras | Eventual + CRDT | Ganho de disponibilidade > custo de resolver conflito |
| Cache de sessão | Read-your-writes | Usuário precisa ver seus próprios updates |
| Auditoria/compliance | Linearizability | Ordem importa para auditor |
| Analytics | Eventual | Aggregates toleram delay e inconsistência minor |
Implementando read-your-writes sem quebrar o banco
📋 App com primary Postgres + 3 read replicas async; equipe reporta 'atualizei mas ao voltar na página apareceu o antigo'
Mantém benefício de read replicas (escalabilidade), não força tudo no primary. Overhead mínimo — um cookie e um check de replay status.
Alt: Sticky to primary por N segundos — simples; derrota o propósito da replica durante a janela
Alt: Strong consistency everywhere — performance tanks; não precisa disso para o resto das queries
Conflict resolution: last-write-wins, CRDT e merge
| Estratégia | Como funciona | Risco |
|---|---|---|
| Last-Write-Wins (LWW) | Timestamp ganha o mais recente; perde outros | Silently drops writes; clock skew perigoso |
| Vector clocks + dequeue de conflitos para app | Identifica concorrência; app decide merge | Complexidade; exige UI de resolução |
| CRDT (Counter, OR-Set, LWW-Map, RGA) | Estruturas matematicamente comutativas; merge automático | Overhead de metadata; não serve para tudo |
| Operational Transform (OT) | Transforma operações concorrentes para manter intenção | Complexo; usado em Google Docs legado |
| Semantic merge | Regras por tipo de dado (carrinho: união; saldo: soma de deltas) | Trabalho de domínio; erro fica invisível |
# OR-Set simples (CRDT) — adiciona e remove sem perder operações concorrentes
from dataclasses import dataclass, field
import uuid
@dataclass
class ORSet:
adds: dict[str, set[str]] = field(default_factory=dict) # item -> set de uids
removes: dict[str, set[str]] = field(default_factory=dict)
def add(self, item: str) -> None:
uid = str(uuid.uuid4())
self.adds.setdefault(item, set()).add(uid)
def remove(self, item: str) -> None:
for uid in self.adds.get(item, set()):
self.removes.setdefault(item, set()).add(uid)
def value(self) -> set[str]:
return {
it for it, uids in self.adds.items()
if uids - self.removes.get(it, set())
}
def merge(self, other: "ORSet") -> "ORSet":
r = ORSet()
for it in set(self.adds) | set(other.adds):
r.adds[it] = self.adds.get(it, set()) | other.adds.get(it, set())
for it in set(self.removes) | set(other.removes):
r.removes[it] = self.removes.get(it, set()) | other.removes.get(it, set())
return rPerguntas típicas
❓ Linearizability é o 'certo' e os outros são compromissos?
❓ Redis é linearizable?
❓ Como testar se meu sistema honra o modelo que eu afirmo?
❓ Cassandra LWW em texto livre é seguro?
Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito