trilha

Tecnologias/02 - Bancos de Dados/02 - Não Relacionais/01 - MongoDB6 min

Transações, Replica Set e Durabilidade

Conceito

A pergunta desta nota é uma só: quando o MongoDB responde "ok" para uma escrita, o que exatamente ele garantiu?

A resposta não é fixa — ela é configurável em cada operação, por três parâmetros. Esse é o ponto que mais diferencia o Mongo de um relacional, onde COMMIT significa uma coisa só.

O trio que define sua consistência

1. writeConcern — quantos nós confirmaram

Valor Significa Risco
w: 0 nem espera resposta perde escrita sem saber
w: 1 o primário gravou failover pode reverter a escrita
w: "majority" a maioria do replica set gravou seguro; é o padrão desde a 5.0
j: true gravado no journal (disco) mais lento, sobrevive a queda de energia

2. readConcern — de qual versão você lê

Valor Significa
local o que o nó tem, mesmo não replicado
majority só o que a maioria já confirmou (não pode ser revertido)
snapshot visão consistente de um instante (necessário em transação)
linearizable leitura garantidamente após toda escrita confirmada; caro

3. readPreference — de qual nó você lê

primary (padrão) · primaryPreferred · secondary · secondaryPreferred · nearest

A regra que evita 80% dos incidentes: w: "majority" + ler do primário. readPreference: secondary é uma otimização de leitura que compra dado atrasado — não é "escalar leitura de graça".

Replica set e failover

  • Só o primário aceita escrita. Secundários aplicam o oplog.
  • Se o primário cai, os nós elegem outro (protocolo tipo Raft) — tipicamente em ~10 s.
  • Escritas com w: 1 que ainda não replicaram podem ser revertidas (rollback) quando o antigo primário voltar como secundário. Com w: "majority", não podem.
  • Retryable writes (padrão nos drivers modernos): o driver repete automaticamente uma escrita que falhou por troca de primário — o que só é seguro porque a operação carrega um id de transação.

Transações multi-documento

Existem desde a 4.0 (replica set) e 4.2 (sharded cluster), com ACID de verdade e readConcern: snapshot:

const session = client.startSession()
try {
  await session.withTransaction(async () => {
    await contas.updateOne({ _id: a }, { $inc: { saldo: -100 } }, { session })
    await contas.updateOne({ _id: b }, { $inc: { saldo:  100 } }, { session })
  }, { readConcern: { level: "snapshot" }, writeConcern: { w: "majority" } })
} finally {
  await session.endSession()
}

withTransaction já embute o retry para TransientTransactionError — o que é obrigatório, porque conflito de escrita aborta a transação e o app precisa tentar de novo.

E a operação sobre um único documento é sempre atômica, sem transação nenhuma:

// atômico por definição, mesmo mexendo em 3 campos e num array
db.contas.updateOne(
  { _id: a, saldo: { $gte: 100 } },
  { $inc: { saldo: -100 }, $push: { extrato: { tipo: "debito", valor: 100 } } }
)

Este é o desenho que o MongoDB quer que você use: se o agregado está num documento, a atomicidade é grátis (2. Modelagem de Documentos).

Trade-offs

Transação existe, mas custa e é limitada. Duração padrão de 60 s (transactionLifetimeLimitSeconds), conflitos abortando sob contenção, custo de coordenação maior em cluster sharded. A posição oficial da MongoDB é honesta: transação é uma saída de emergência, não a ferramenta principal.

Se o seu sistema precisa de transação multi-documento em toda operação de escrita, o modelo de dados está relacional — e você provavelmente escolheu o banco errado. Ver 4. MongoDB vs Relacional.

w: "majority" custa latência. Você espera o round-trip até a maioria dos nós. É o preço de não perder escrita, e é o mesmo trade-off de Availability vs Consistency.

Ler do secundário parece grátis e não é. Você troca latência por staleness. Fluxos "escreve e imediatamente lê" (criar um recurso e devolver ele na resposta) quebram — e quebram de forma intermitente, dependendo do lag. Só use secundário para leitura que tolera atraso: relatório, listagem, busca.

Onde o MongoDB fica no CAP Theorem: com w: majority + readConcern: majority + leitura no primário, ele é CP — sob partição, o lado minoritário para de aceitar escrita. Afrouxando os parâmetros, você caminha para AP. É o mesmo sistema em pontos diferentes do espectro, escolhidos por configuração. Ver Eventual Consistency.

Exemplo prático

O mesmo UPDATE com três garantias diferentes — e o que cada uma custa:

// 1) Telemetria: perder um ponto não importa. Rápido.
db.metricas.insertOne(doc, { writeConcern: { w: 0 } })

// 2) Padrão de aplicação: não perder escrita em failover.
db.pedidos.insertOne(doc, { writeConcern: { w: "majority" } })  // já é o default

// 3) Financeiro: sobreviver a queda de energia no datacenter.
db.lancamentos.insertOne(doc, { writeConcern: { w: "majority", j: true } })

E o padrão que substitui transação na maioria dos casos — idempotência por chave natural:

// Reprocessar a mesma mensagem do Kafka N vezes produz o mesmo resultado.
db.pedidos.updateOne(
  { _id: msg.pedido_id },                 // a chave natural do evento
  { $setOnInsert: { criado_em: new Date() },
    $set: { status: msg.status, atualizado_em: new Date() } },
  { upsert: true }
)

Uma operação, atômica, sem transação, segura sob reentrega. É a ponte entre esta seção e 4. Garantias de Entrega e Idempotência — e o motivo pelo qual Idempotent Operations é o padrão mais valioso de sistemas distribuídos.

Como isso falha em produção

Sintoma Causa
Escrita "desapareceu" depois de um failover w: 1: o primário confirmou e caiu antes de replicar
Recurso criado não aparece no GET seguinte readPreference: secondary com lag
WriteConflict / TransientTransactionError contenção em transação — precisa de retry (use withTransaction)
Transação abortando em 60 s trabalho demais dentro da transação; quebre em partes idempotentes
Réplica exigindo initial sync completo ficou fora mais tempo que a janela do oplog
Latência de escrita subiu sem mudar o código um nó lento faz w: majority esperar por ele
Escrita revertida silenciosamente rollback de oplog do antigo primário
Dimensione a janela do oplog pelo pior caso de manutenção

Se um upgrade de nó leva 4 horas, a janela do oplog precisa cobrir 4 horas de escrita — senão a réplica volta e precisa de sincronização total, justamente no pior momento.

Aprofundamento aplicado

Configurar a jornada de leitura depois de escrita

Escolher um secondary para leitura define preferência de roteamento. Escolher write concern define o que uma confirmação de escrita exige. Read concern define propriedades da visão lida. São controles relacionados, mas nenhum substitui automaticamente os demais.

Cenário: uma atualização confirma e uma tela lê em outro nó. A jornada precisa definir se aceita atraso ou se requer observar a própria escrita, usando as garantias e sessões apropriadas. “Há três réplicas” não responde a essa pergunta.

Falha durante o commit

Um timeout pode deixar o cliente sem saber se a transação confirmou. Os drivers fornecem mecanismos e classificações para retry de transações e commits; a aplicação deve seguir o contrato do driver. Não coloque um efeito externo não idempotente dentro de um callback que pode ser repetido.

Exercício: desenhe pedido, reserva e envio de confirmação. Resultado esperado: alterações que exigem atomicidade ficam na transação; a comunicação externa tem registro durável e identificação para lidar com reexecução.

Referências: transações e consistência causal.

Relacionado


Parte de MOC MongoDB · Transações (MongoDB)

Buscar

Busca por título, seção e texto das notas