Por Flank Manoel da Silva Especialista Sênior Full Stack | Analista de Infraestrutura e Performance
Eu vou direto ao ponto: o seu distributed lock não falha quando “dá erro”. Ele falha quando parece funcionar. Em sistemas financeiros, isso é pior do que um crash, porque você não ganha alerta, você ganha reconciliação manual, chargeback, auditoria e uma fila de clientes furiosos no suporte.
O cenário clássico é o “saldo” (wallet, gift card, store credit, rewards) sendo atualizado por múltiplos workers em paralelo, em Kubernetes, atrás de autoscaling, com Redis/Redlock ou Etcd “garantindo exclusão mútua”. Aí você coloca pressão real (picos tipo Black Friday/Cyber Monday), introduz latência de rede e… pronto: race conditions silenciosas começam a vazar por uma brecha que quase ninguém modela direito.
Essa brecha tem nome: TTL do lock versus pausas de runtime (GC / stop-the-world) e jitter de scheduler. E sim, eu já vi isso acontecer em stacks “boas” (Java/Kotlin, Node.js, Go, .NET) com APM (Datadog/New Relic) aparentemente “verde”.
O resumo que eu daria ao meu time antes do deploy
Se você só levar 5 ideias daqui, leve estas:
- O distributed lock só protege enquanto você consegue provar que ainda “possui” o lock no momento exato da escrita. Muita implementação não prova — ela “assume”.
- TTL é uma aposta, não uma garantia. Se o seu processo congelar (GC, CPU throttling, noisy neighbor, pausa de VM, page fault), o TTL expira e outro worker pega o distributed lock. O primeiro “volta do coma” e escreve mesmo assim.
- Split-brain e partição de rede transformam lock em ilusão estatística. “Baixa probabilidade” vira “evento semanal” quando você roda milhões de operações.
- Banco relacional não “barra” automaticamente esse tipo de corrupção. Se você faz update ingênuo (sem fencing token/versão), o DB aceita a escrita velha como se fosse nova.
- A correção real quase sempre envolve um mecanismo de fencing (token monotônico) + escrita condicional, ou um desenho que evite concorrência por chave (single-writer / partitioning / ledger). Sim: ainda dá para usar distributed lock, mas ele vira só uma camada, não a parede principal.
Como eu quebrei um saldo “protegido” com split-brain + latência artificial
Eu montei um ambiente bem parecido com o que vejo na prática nos EUA:
- Workers em Kubernetes (EKS), HPA ligado, múltiplas réplicas.
- Redis em cluster (pense em ElastiCache/Redis Enterprise como equivalente operacional), com Redlock.
- Uma rota crítica: “applyCredit(accountId, amount)” atualizando o saldo.
- Tráfego concorrente simulando checkout + ajustes de backoffice + reprocessamento de eventos.
O que eu fiz de diferente foi tratar a rede como inimiga, não como pano de fundo.
O teste que revela o “abismo da consistência”
Eu forcei três coisas ao mesmo tempo:
- Contenção real por chave (várias requisições no mesmo accountId).
- Partição/latência entre nós do cluster Redis (para simular split-brain e timeouts).
- Pausas longas no runtime do worker (GC/stop-the-world) para o TTL perder a corrida.
Exemplo (conceitual) do que eu injetei com tc (Linux netem) em um dos nós/paths relevantes:
- Atraso médio: 250ms
- Jitter: 200ms
- Perda: 1–2%
- Reordenação ocasional
O objetivo não era “derrubar” o sistema. Era deixá-lo vivo, porém mentiroso. Essa instabilidade muitas vezes não nasce no código, mas na camada de rede, como acontece nos gargalos gerados pela convivência entre NAT/CGNAT e IPv6, onde a tradução de protocolos adiciona uma latência imprevisível.
O bug que aparece quando ninguém está olhando
O fluxo típico que eu encontro em times bons é algo assim:
- Worker A pega o distributed lock com TTL = 2s
- Worker A faz lógica, chama serviços, calcula, prepara update
- Em algum ponto, Worker A sofre uma pausa de 3–8s (GC, CPU throttling)
- TTL expira
- Worker B pega o distributed lock e atualiza o saldo
- Worker A “acorda” e faz o update atrasado
- Tudo retorna 200 OK (às vezes com logs “normais”)
Isso é a definição de race condition silenciosa: ninguém “quebrou”, mas a ordem lógica dos eventos foi violada.
Ilustração 1 — Linha do tempo (TTL vs pausa do runtime)
Tempo →
Worker A: lock acquired |—— do work ——| [GC PAUSE] | resumes | WRITE (late)
TTL: |—- 2s —-| EXPIRES
Worker B: lock acquired | do work | WRITE (new)
Resultado: WRITE velho chega depois do novo, mas parece válido
Por que isso virou urgência agora para o americano comum (não só “problema de big tech”)
Eu vejo três fatores tornando o distributed lock mais traiçoeiro em 2026 do que era “anos atrás”:
- Kubernetes + autoscaling aumentam a frequência de “pausas estranhas”Você tem:
- Pods sendo throttled por CPU limit
- Node pressure (memory/IO)
- Evictions e reschedules
- Cold start de dependênciasTudo isso cria janelas em que o TTL do distributed lock vira uma roleta.
- Finanças embutidas (embedded finance) virou padrão. Não é só banco. É:
- marketplaces com saldo interno
- apps de delivery com créditos
- plataformas de rewards
- gift cards e store credit no e-commerceIsso aumenta o volume de “saldo compartilhado” e o custo do erro.
- Observabilidade costuma medir média e p95 — mas esse bug mora no p99.99GC pause e jitter de rede que “não aparecem” no dashboard comum ainda são suficientes para estourar o TTL do distributed lock, e é ali que o dinheiro escapa.
Dois perfis que mais sofrem (e por motivos diferentes)
Eu vou segmentar em dois perfis bem típicos nos EUA, porque eles quebram de jeitos diferentes:
Perfil 1: Checkout de alto volume (Shopify-like, Stripe-like, promoções agressivas)
Aqui o problema é concorrência por chave em tempo real:
- múltiplas tentativas de pagamento
- retries de gateway
- idempotência parcial
- carrinho + wallet + rewards batendo no mesmo saldo
Se o distributed lock falha, você ganha: saldo negativo indevido, double-spend de crédito e reconciliação com parceiros e chargebacks.
Perfil 2: Processamento assíncrono (event-driven, filas, reprocessamento)
Aqui o problema é “ordem” e “replay”:
- reprocessar eventos
- consumer lag
- redelivery
- dead-letter reingestion
Mesmo com distributed lock, se você não tiver fencing/versão, o worker atrasado pode sobrescrever o estado correto.
Tabela 1 — Onde o distributed lock engana mais
| Característica | Checkout alto volume | Assíncrono/reprocessamento |
| “Contenção por chave” | Alto (mesmo accountId) | Médio (depende do particionamento) |
| “Probabilidade de retries” | Alta (timeouts, gateways) | Alta (redelivery, replay) |
| “Dano por escrita fora de ordem” | Muito alto (dinheiro agora) | Alto (corrupção lenta, difícil de detectar) |
| “Sintoma típico” | Saldo errado imediatamente | Divergência depois de horas/dias |
| “Armadilha mais comum” | TTL curto para performance | Falta de versionamento na projeção |
O lock expira, mas seu processo continua se achando dono
A venda implícita do distributed lock em muitos times é: “pegou lock, então está seguro”. Não está.
O Redlock (documentação pública) tenta endereçar parte do problema ao exigir quórum de nós para adquirir o lock. Só que a crítica do Martin Kleppmann não é sobre “ser impossível adquirir lock com quórum”. É sobre algo mais sutil: você não consegue transformar “eu acredito que tenho o lock” em “eu provo que ainda tenho o lock no instante da escrita”, se seu mecanismo não inclui fencing e se seu sistema sofre pausas e partições.
O que eu vejo times errarem na prática:
TTL calculado com base em “tempo médio de request” (erro clássico)
Eles olham p95 do handler e falam “2s dá e sobra”. Aí o runtime dá uma pausa de 6s porque:
- JVM Full GC
- Node.js com stop-the-world em heap grande
- Go com GC + CPU throttling
- container com CPU limit baixo e burst esgotadoResultado: o distributed lock expira no “pior momento possível”.
Renovação de lock (watchdog) que falha silenciosamente
Alguns implementam renew (estilo “keep-alive”). Só que:
- durante GC o thread/event-loop não roda, então não renova
- durante partição de rede, a renovação não chega
- durante overload, a renovação atrasaOu seja: o watchdog dá a sensação de segurança e só.
“Eu libero no finally” não ajuda quando o lock já expirou
Essa é a parte que eu mais vejo em review de PR: “pego o distributed lock, faço, libero no finally”. Ótimo para higiene… irrelevante para o bug principal. O bug ocorre antes do finally.
Por que “atualizar saldo” é um convite ao desastre
Aqui vai a diferença que separa sistemas que sobrevivem de sistemas que fazem auditoria com dor.
Cenário A: saldo como número mutável (update in-place)
Você tem uma linha “accounts.balance” e faz updates concorrentes. Mesmo com distributed lock, basta um worker atrasado escrever depois e você corrompe a verdade.
Ilustração 2 — Update in-place (onde a corrupção passa)
Worker A: lê 100, aplica -30, quer escrever 70
Worker B: lê 100, aplica -10, escreve 90
Worker A: escreve 70 (tarde)
Final: 70, mas deveria ser 60 se ambos fossem válidos e ordenados corretamente, ou 90/70 dependendo da semântica. Você perdeu a trilha.
Cenário B: ledger (razão) append-only + projeção
Você registra eventos imutáveis (“debit 30”, “debit 10”) com idempotency key, e o saldo é projeção. O distributed lock pode até existir para reduzir contenção na projeção, mas a fonte de verdade não depende dele. O estrago fica muito mais difícil.
Conclusão prática: se o seu produto tem “dinheiro” (mesmo store credit), ledger reduz sua dependência de distributed lock como item “sagrado”.
O protocolo que eu aplico quando o time insiste em usar distributed lock
Eu não sou “anti-lock”. Eu sou anti-mito. Se você vai usar distributed lock em fluxo financeiro, aqui está meu protocolo mínimo para você parar de depender de sorte.
Passo 1: Meça o que realmente mata TTL (p99.99, não p95)
Colete e guarde por pelo menos 7 dias (incluindo picos):
- GC pause p99.99 (JVM: gc logs; Go: runtime/metrics; .NET: ETW; Node: perf hooks)
- event-loop lag p99.99 (Node)
- container CPU throttling time
- network RTT entre worker e Redis p99.9
- redis command latency p99.9
- timeouts e retries reaisRegra que eu uso: se você não sabe seu p99.99 de pausas, você não escolheu TTL; você chutou.
Passo 2: Pare de tratar TTL como “timeout”; trate como “contrato de validade”
Você precisa responder: “Se o contrato expirar, como eu impeço escrita tardia?” Se a resposta for “não impede”, então seu distributed lock é só um rate limiter chique.
Passo 3: Adote fencing token (token monotônico) e torne a escrita condicional
Esse é o ponto que separa “lock de blog” de engenharia que segura pancada.
Fluxo robusto (conceito):
- Ao adquirir o distributed lock, você recebe um token monotônico (fencing token).
- Toda escrita no banco inclui esse token.
- O banco só aceita se token > last_token.Isso transforma “ordem lógica” em regra verificável no storage.
Ilustração 3 — Fencing token salvando seu pescoço
- Worker A pega token 101 (mas congela)
- Worker B pega token 102 e escreve (aceito)
- Worker A volta e tenta escrever com token 101 (rejeitado pelo DB)Resultado: o atraso não corrompe o estado. Sem isso, seu distributed lock está baseado em fé.
Passo 4: Faça a operação ser idempotente de verdade (não “mais ou menos”)
Em checkout americano, retries são inevitáveis (gateway, rede, user refresh, mobile switching). Se você mistura distributed lock com não-idempotência, você multiplica dano.
- idempotency key por operação financeira
- unique constraint no ledger/event store
- retry seguro no consumer (exatamente-uma-vez é promessa cara; prefira pelo menos-uma-vez + idempotência)
Passo 5: Injete caos como rotina, não como evento
Eu não confio em distributed lock que não passou por:
tc netem(latência, jitter, loss)- CPU throttling proposital (reduzir limit do container)
- pauses artificiais (sleep em pontos críticos, simular stop-the-world)
kill -STOP/kill -CONT(em ambiente controlado) para simular congelamento- testes com clock skew se sua infra permitir variação (NTP drift)
Gráfico 1 — “Mapa de risco” (quanto maior, pior)
Eixo X: Pausas de runtime (GC / throttling) → baixo para alto
Eixo Y: Instabilidade de rede (jitter / loss / partição) → baixo para alto
- Quadrante baixo/baixo: distributed lock “parece perfeito”
- Quadrante alto/baixo: TTL perde para GC, corrupção silenciosa
- Quadrante baixo/alto: split-brain/timeouts, duplicidade e reorder
- Quadrante alto/alto: o lock vira ruído; só fencing/ledger segura
Onde eu permito distributed lock (e onde eu veto) depois de ver sistemas sangrarem
Eu permito distributed lock quando:
- o recurso é “best-effort” (ex: gerar PDF, cache warmup)
- o dano de uma execução duplicada é baixo e reversível
- há idempotência forte
- o storage final tem proteção contra escrita tardia (version check / fencing)Nesses casos, distributed lock é útil como “controle de concorrência operacional”.
Eu veto distributed lock como “única defesa” quando:
- há dinheiro/saldo/limite/estoque que vira dinheiro
- há múltiplos writers e latência variável
- o runtime tem GC relevante (heap grande, picos)
- você depende de “update in-place” no DBNesses casos, distributed lock sem fencing é convite para corrupção com recibo de sucesso (HTTP 200).
A decisão de longo prazo: o que custa menos, de verdade (não no PowerPoint)
Aqui é onde eu sou mais cético: o mercado empurra distributed lock porque parece mais barato do que mexer no modelo de dados. Só que a conta chega.
- Custo imediato (aparente): “coloca Redis lock e pronto”
- Custo real (12–18 meses): incidentes estranhos, reconciliação, perda de confiança, auditoria, retrabalho no core financeiro
Quando você investe em ledger + idempotência, fencing token no storage, projeções reconstruíveis e particionamento por chave (single-writer por accountId), você reduz a área onde distributed lock é necessário. E isso é o tipo de arquitetura que aguenta Black Friday, falha parcial de AZ, jitter de rede, pods congelando e retries de gateway.
Meu veredito: use distributed lock como camada auxiliar, nunca como garantia final de integridade. Em finanças, a garantia final precisa estar no storage (escrita condicional) ou no desenho (single-writer/ledger). Se você tratar o distributed lock como “o guardião”, você está terceirizando sua consistência para o relógio e para a sorte.
Conclusão:
O grande erro da última década foi tratar o distributed lock como uma solução mágica de “um clique”, quando ele é, na verdade, uma aposta contra o relógio. Em sistemas de baixa escala, essa aposta quase sempre ganha. Mas, quando você escala para milhões de transações, a estatística se torna cruel: o que era “improvável” vira um incidente de severidade 1 na sua madrugada de segunda-feira.
Se você gerencia infraestrutura web ou desenha sistemas que movem valores, sejam eles financeiros, estoque ou dados críticos, a regra de ouro é: nunca terceirize sua integridade apenas para o tempo.
O checklist de saída para sua próxima revisão de arquitetura:
- Assuma a falha do Lock: Desenhe seu banco de dados para rejeitar escritas que não provem posse do lock (use Fencing Tokens).
- Imutabilidade acima de tudo: Migre de saldos mutáveis para modelos de Ledger (Razão). É mais fácil somar eventos do que tentar descobrir quem corrompeu um valor fixo.
- Monitore o invisível: Se você não mede as pausas de GC e o jitter da sua rede, você não tem um sistema sob controle, você tem um sistema que “ainda não quebrou”.
No final do dia, a diferença entre um desenvolvedor e um engenheiro de infraestrutura é que o segundo sabe que a rede vai falhar, o processo vai travar e o relógio vai mentir. O distributed lock é uma ferramenta útil para evitar trabalho duplicado, mas é uma defesa pífia para proteger a integridade do dado.
Construa sistemas que não precisem de sorte para serem consistentes.





