Conflict resolution em sistemas eventual consistentes

1. Fundamentos da Consistência Eventual e a Origem dos Conflitos

A consistência eventual é um modelo de consistência fraco onde, na ausência de novas escritas, todas as réplicas de um sistema distribuído convergem para o mesmo estado. Diferente da consistência forte, que garante que toda leitura retorne a escrita mais recente, sistemas eventualmente consistentes aceitam que réplicas possam divergir temporariamente.

As causas dos conflitos são variadas: concorrência entre escritas simultâneas em diferentes nós, latência de rede que atrasa a propagação de atualizações, e falhas de partição que isolam nós uns dos outros. O teorema CAP nos mostra que em sistemas distribuídos, precisamos sacrificar consistência forte para manter disponibilidade e tolerância a partições — daí surge a necessidade de estratégias de resolução de conflitos.

2. Estratégias de Detecção de Conflitos

Versionamento Vetorial

Vector clocks são mecanismos que rastreiam o histórico causal de atualizações. Cada nó mantém um contador lógico, e o clock é um vetor de pares (nó, contador). Quando dois clocks são incomparáveis (nenhum é ancestral do outro), detectamos um conflito.

// Representação de vector clock
Nó A: (A:3, B:2, C:1)
Nó B: (A:2, B:4, C:1)
// Conflito detectado: nenhum clock é descendente do outro

Dotted Version Vectors

Para ambientes com muitos nós, dotted version vectors melhoram a escalabilidade ao associar versões a intervalos de eventos, reduzindo o tamanho dos vetores.

3. Abordagens de Resolução Automática de Conflitos

Last-Writer-Wins (LWW)

A estratégia mais simples: o valor com timestamp mais recente vence. Embora eficiente, LWW pode perder dados importantes se timestamps físicos não forem sincronizados.

// Exemplo de LWW com timestamps lógicos
Documento versão 1: {conteudo: "A", timestamp: 100}
Documento versão 2: {conteudo: "B", timestamp: 105}
Resultado LWW: {conteudo: "B", timestamp: 105}

CRDTs (Conflict-free Replicated Data Types)

CRDTs garantem convergência através de operações comutativas. Tipos como G-Counter (contador crescente) e OR-Set (conjunto com suporte a remoção) permitem que réplicas atualizem independentemente e depois mesclem sem conflitos.

// G-Counter replicado entre dois nós
Nó 1: [3, 0]  -> incrementou 3 vezes localmente
Nó 2: [0, 5]  -> incrementou 5 vezes localmente
Merge: [max(3,0), max(0,5)] = [3, 5] -> total = 8

Merge Functions Customizadas

Para dados complexos, podemos definir funções de merge específicas do domínio:

function mergeLogs(logA, logB):
    return logA.concat(logB).sortByTimestamp().deduplicate()

4. Resolução Manual e Intervenção do Usuário

Quando a resolução automática não é adequada, expomos o conflito ao usuário:

// Estrutura de conflito para UI
Conflito: {
  campo: "nome",
  versao_local: "João Silva",
  versao_remota: "João S.",
  timestamp_local: 1700000000,
  timestamp_remoto: 1700000100
}
// UI exibe ambas opções para o usuário escolher

Heurísticas automáticas com fallback manual combinam eficiência com precisão. Armazenar o histórico de conflitos permite auditoria e rollback caso a resolução escolhida se mostre incorreta.

5. Estratégias de Resolução Específicas por Tipo de Dado

Valores Escalares

// Estratégias para valores numéricos
Substituição: conflito.preco = ultimo_valor
Média: conflito.temperatura = (valorA + valorB) / 2
Soma: conflito.contador = valorA + valorB

Listas e Conjuntos

// Merge de operações em lista ordenada
Lista A: insert("item1", pos=0), insert("item2", pos=1)
Lista B: insert("item3", pos=0), delete("item1")
Merge: ["item3", "item2"]  // operações comutativas aplicadas

Documentos Aninhados

Merge recursivo de campos, aplicando estratégias diferentes por tipo de campo:

{
  "usuario": { "nome": "Maria", "email": "m@ex.com" },
  "preferencias": { "tema": "escuro", "idioma": "pt" }
}
// Merge campo a campo, com LWW para escalares

6. Padrões de Arquitetura para Minimizar Conflitos

Sharding por Usuário ou Região

Particionar dados para que escritas concorrentes em um mesmo registro sejam raras:

// Roteamento por hash do usuário
shard_id = hash(usuario_id) % NUM_SHARDS
// Cada shard é mestre para seus dados

Operações Idempotentes e Comutativas

Projetar APIs que possam ser repetidas sem efeitos colaterais:

// Idempotente: mesmo resultado independente do número de execuções
PUT /usuario/123/saldo { "valor": 100 }
// Não idempotente: resultado depende do estado anterior
POST /usuario/123/saldo/incrementar { "valor": 10 }

Locks Otimistas e Transações Compensatórias

Locks otimistas detectam conflitos antes de confirmar escritas, enquanto transações compensatórias desfazem operações em caso de falha.

7. Implementação Prática com Exemplos de Código

Exemplo 1: Vector Clock + Merge Function

class DocumentoColaborativo:
    def __init__(self, id, conteudo):
        self.id = id
        self.conteudo = conteudo
        self.vector_clock = {}

    def atualizar(self, no_id, novo_conteudo):
        self.conteudo = novo_conteudo
        self.vector_clock[no_id] = self.vector_clock.get(no_id, 0) + 1

    def merge(self, outro_documento):
        # Verifica conflito causal
        if self.eh_ancestral(outro_documento):
            return outro_documento
        if outro_documento.eh_ancestral(self):
            return self

        # Conflito detectado: merge manual ou automático
        return self.resolver_conflito(outro_documento)

Exemplo 2: G-Counter CRDT

class GCounter:
    def __init__(self, no_id, num_nos):
        self.no_id = no_id
        self.contadores = [0] * num_nos

    def incrementar(self, valor=1):
        self.contadores[self.no_id] += valor

    def valor(self):
        return sum(self.contadores)

    def merge(self, outro):
        for i in range(len(self.contadores)):
            self.contadores[i] = max(
                self.contadores[i], 
                outro.contadores[i]
            )

Exemplo 3: Resolução Manual com UI

def detectar_e_resolver_conflito(documento_local, documento_remoto):
    if documento_local.vector_clock == documento_remoto.vector_clock:
        return documento_local  # idênticos

    if documento_local.eh_ancestral(documento_remoto):
        return documento_remoto
    if documento_remoto.eh_ancestral(documento_local):
        return documento_local

    # Conflito: salva para resolução manual
    conflito = {
        "documento_id": documento_local.id,
        "versao_local": documento_local.conteudo,
        "versao_remota": documento_remoto.conteudo,
        "timestamp_local": time.time(),
        "timestamp_remoto": documento_remoto.timestamp
    }

    # Notifica usuário ou aplica heurística
    if politica_auto_resolucao(conflito):
        return aplicar_heuristica(conflito)
    else:
        salvar_para_resolucao_manual(conflito)
        return None  # pendente

8. Monitoramento, Testes e Validação de Conflitos

Simulação de Cenários

Testes de concorrência devem incluir:

# Cenários de teste
1. Duas escritas simultâneas no mesmo campo
2. Escrita em nó isolado seguida de reconexão
3. Cascata de atualizações com múltiplos nós
4. Conflitos em documentos aninhados profundamente

Métricas Essenciais

  • Taxa de conflitos por registro por hora
  • Tempo médio de resolução (automática vs. manual)
  • Percentual de conflitos resolvidos automaticamente
  • Latência de propagação entre réplicas

Ferramentas de Observabilidade

Rastrear versões e merges com logs estruturados:

{
  "evento": "merge_realizado",
  "documento_id": "doc_123",
  "versao_resultante": "v5",
  "versoes_fonte": ["v3_nóA", "v4_nóB"],
  "estrategia": "LWW",
  "timestamp": 1700000100
}

Sistemas eventualmente consistentes são uma realidade em arquiteturas distribuídas modernas. A escolha da estratégia de resolução de conflitos depende do domínio do problema, da tolerância a perda de dados e da experiência do usuário desejada. Combinar detecção robusta com estratégias de resolução adequadas — automáticas e manuais — é fundamental para construir sistemas confiáveis e escaláveis.

Referências


Sistemas eventualmente consistentes são uma realidade em arquiteturas distribuídas modernas. A escolha da estratégia de resolução de conflitos depende do domínio do problema, da tolerância a perda de dados e da experiência do usuário desejada. Combinar detecção robusta com estratégias de resolução adequadas — automáticas e manuais — é fundamental para construir sistemas confiáveis e escaláveis.

A implementação bem-sucedida de resolução de conflitos não é um esforço único, mas um processo contínuo de refinamento. À medida que o sistema evolui, novos padrões de uso podem revelar conflitos imprevistos, exigindo ajustes nas heurísticas de merge ou na interface de resolução manual. Por isso, recomenda-se:

  • Automatizar o máximo possível, mas sempre manter um mecanismo de fallback manual para casos excepcionais.
  • Documentar cada estratégia de resolução adotada, incluindo trade-offs e cenários de uso esperados.
  • Testar sob carga realista, simulando partições de rede, latência variável e escritas concorrentes.
  • Monitorar continuamente as taxas de conflito e o tempo de resolução, estabelecendo alertas para desvios significativos.

Ao dominar essas técnicas, arquitetos e desenvolvedores podem aproveitar os benefícios da consistência eventual — alta disponibilidade, baixa latência e escalabilidade horizontal — sem comprometer a integridade dos dados.