Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

02 - Estratégias (Cache-Aside, Write-Through, Write-Behind)

#cache-aside#consistencia#read-through#write-behind#write-through
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Estratégias (Cache-Aside, Write-Through, Write-Behind)

Estratégias de cache são os padrões de coordenação entre aplicação, cache e banco de dados que definem quando o cache é lido, quando é escrito e o que acontece quando os dois lados divergem. A escolha não é detalhe de implementação: ela determina a consistência observável pelo usuário, a latência das escritas, o risco de perda de dados em falha e a complexidade do código. Cache-aside é o padrão dominante na prática, com leitura lazy e responsabilidade da aplicação; write-through prioriza consistência ao custo de latência de escrita; write-behind troca durabilidade por desempenho de escrita, movendo o risco para dentro do cache. Completam o conjunto read-through, que empurra a população do cache para dentro da abstração, e refresh-ahead, que antecipa a renovação de chaves quentes. Este arquivo detalha cada padrão com seu mecanismo, seus riscos e seu caso de uso dominante, e fecha com a comparação estruturada por consistência e complexidade que orienta a decisão por fluxo de dado, e não por sistema inteiro.

Conceito e o eixo central de decisão

Toda estratégia responde a três perguntas: quem decide o que entra no cache (a aplicação ou a camada de acesso), quando o dado vai para o banco (na mesma operação do cache ou depois) e o que o leitor aceita de obsolescência. As respostas formam o eixo consistência versus latência versus risco de perda, e não existe combinação que maximize as três dimensões ao mesmo tempo.

O erro clássico de desenho é escolher uma única estratégia para o sistema inteiro. Fluxos de leitura pesada com tolerância a defasagem (catálogo, feed) pedem cache-aside; fluxos de escrita crítica (saldo, estoque reservado) pedem write-through ou escrita direta sem cache; jobs de alta taxa de escrita agregada (métricas, contadores) são o nicho do write-behind. A estratégia é propriedade do fluxo, e sistemas maduros misturam padrões com fronteiras explícitas.

A invalidação é o conector entre os padrões, e ela não pertence a nenhum deles por padrão. Cache-aside precisa que a aplicação invalide; write-through mantém o cache em passo por construção; write-behind propaga a defasagem como característica. Por isso este arquivo e o próximo (invalidação de cache) são leitura contínua: escolher a estratégia sem desenhar a invalidação é desenhar metade do sistema.

Cache-aside (lazy loading) e o risco de stale

No cache-aside, a aplicação consulta o cache primeiro; em hit, o dado vem do cache; em miss, a aplicação busca no banco, popula o cache e responde. A escrita, por sua vez, atualiza o banco e invalida (ou reescreve) a chave do cache. Nada entra no cache sem ser previamente solicitado, e por isso o padrão também se chama lazy loading: apenas o dado que alguém pediu ocupa memória.

A vantagem estrutural é a resiliência: se o cache cai, o sistema degrada ao banco, mais lento, mas correto. A memória só guarda o que é usado, e a implementação cabe em meia dúzia de linhas. É por isso que cache-aside é o padrão de partida de quase toda aplicação.

O risco que define o padrão é o stale: entre a escrita no banco e a invalidação da chave, uma leitura pode servir a versão antiga, e entre a invalidação e a próxima leitura, outra leitura concorrente pode repopular o cache com dado ainda antigo. Em sistemas de um único escritor com invalidação síncrona, a janela é de milissegundos; com múltiplos escritores e invalidação assíncrona, a janela cresce e precisa ser aceita explicitamente.

A recomendação operacional para escrita em cache-aside é invalidar, não atualizar. Atualizar a chave na escrita cria risco de ordem invertida (a atualização do cache chegar antes do commit do banco) e escreve dados que ninguém pediu; deletar a chave deixa o próximo leitor repopular a partir do estado commitado, simples e correto na maioria dos casos.

O segundo risco do padrão é o stampede de leitura (thundering herd): quando uma chave quente expira, todas as requisições simultâneas sofrem miss ao mesmo tempo e todas atacam o banco para repopular. As contra-medidas, locking de população com SET NX e expiração probabilística, são tratadas em detalhe no arquivo de invalidação; aqui basta registrar que todo cache-aside em produção com chaves quentes precisa delas.

# Cache-aside com invalidação por deleção após o commit
def atualizar_estoque(pid: int, delta: int):
    db.executar("UPDATE estoque SET qtd = qtd + %s WHERE pid = %s", delta, pid)
    cache.delete(f"estoque:v1:{pid}")     # invalida, próximo leitor repopula

def ler_estoque(pid: int) -> dict:
    chave = f"estoque:v1:{pid}"
    valor = cache.get(chave)
    if valor is not None:
        return valor
    registro = db.buscar_estoque(pid)
    cache.set(chave, registro, ex=60)     # TTL curto como segunda defesa
    return registro

Um refinamento do cache-aside é o negative caching: respostas vazias ou de "não existe" também são armazenáveis com TTL curto, evitando que chaves inexistentes ou consultas sem resultado virem miss permanente atacando o banco a cada leitura.

O TTL no cache-aside não é opcional mesmo com invalidação por evento: ele é a rede de segurança contra invalidação perdida, bug de chave e qualquer divergência silenciosa entre banco e cache. A combinação invalidação por escrita mais TTL curto é a configuração padrão de produção.

Write-through: consistência como prioridade

No write-through, a escrita passa primeiro pelo cache, que sincronamente grava no banco antes de confirmar a operação. Cache e banco ficam sempre em passo, e qualquer leitura subsequente, por qualquer instância, encontra o dado atualizado. A consistência observável é a mais forte possível entre os dois armazenamentos.

O custo é direto: toda escrita paga a latência dos dois destinos em série, e a maioria dessas escritas popula cache de dado que talvez ninguém leia, desperdiçando memória com chaves frias. Escrita em write-through é tipicamente 2 a 3 vezes mais lenta que escrita direta no banco.

O caso de uso dominante é o dado escrito e imediatamente re-lido: sessões atualizadas a cada requisição, configuração lida com frequência, perfis de usuário que a tela seguinte renderiza. Também é o padrão natural quando a camada de acesso já centraliza leitura e escrita (um cache provider ou biblioteca de repositório), pois o write-through vira uma linha de configuração.

O ponto fraco operacional é a dependência: se o cache está fora do ar, a escrita falha ou precisa de fallback explícito para o banco. Como o cache está no caminho crítico da escrita, sua disponibilidade passa a ser requisito da operação de negócio, e não apenas aceleração de leitura.

A variação write-through com repopulação ativa na escrita tem versão utilitária comum: escrever no banco e reescrever a chave do cache em seguida (update-aside na escrita). É tecnicamente cache-aside com população ativa, mantém leitura rápida imediatamente após a escrita e evita a serialização dos dois destinos; a diferença prática frente ao write-through clássico é a ordem (banco primeiro) e o custo de reescrever o payload do cache a cada atualização.

Write-behind (write-back): desempenho com risco de perda

No write-behind, a aplicação escreve no cache e confirma imediatamente; o cache, de forma assíncrona, empurra a escrita para o banco depois, em lote ou por política de atraso. A latência percebida pela aplicação é a do cache, e o banco recebe escritas consolidadas, o que reduz tanto a latência p99 quanto o volume de I/O.

O risco é estrutural e inegociável: uma escrita confirmada que ainda não chegou ao banco se perde se o cache cair antes do flush. Janelas de alguns segundos a minutos de perda potencial são o preço do padrão, e por isso write-behind em dados de valor monetário ou irreversível é, em geral, erro de arquitetura.

O nicho legítimo é dado de alta taxa, agregável e tolerante a perda: métricas por segundo, contadores de visualização, presença e telemetria. Nesses fluxos, perder os últimos segundos em desastre é aceitável, e a consolidação em lote (múltiplos increments viram um único update) é ganho real de escala.

# Write-behind: confirmação no cache, flush assíncrono ao banco
contador = cache.incr("views:video:981")       # resposta imediata ao usuário
if contador % 50 == 0:                          # flush por lote de 50
    fila_flush.enqueue("views:video:981", contador)
    # o worker da fila persiste o valor consolidado no banco

# Regra do padrão: o que confirma no cache precisa ter janela declarada
# de perda máxima aceitável, e o flush precisa de alarme quando atrasa.

A operação do write-behind exige telemetria específica: tamanho da fila de flush, atraso máximo observado entre confirmação no cache e persistência, e alarme quando a janela excede a perda aceita. Sem isso, o padrão falha em silêncio e o dado some sem rastro.

A mitigação parcial do risco vem da durabilidade do próprio cache: Redis com AOF e fsync configurado, ou com réplicas síncronas, encurta drasticamente a janela de perda, e a confirmação ao cliente só acontece depois da persistência local. O padrão então deixa de ser "confiar na memória volátil" e vira "confiar na durabilidade do cache", com custo de latência proporcional; ainda assim é uma janela, apenas menor, e a análise de risco continua sendo por fluxo.

Read-through e refresh-ahead

O read-through desloca a lógica do cache-aside para dentro de uma abstração: a aplicação pede dado a um provedor de cache, e o provedor, em miss, busca na fonte, popula e devolve. O código da aplicação nunca fala com o banco na leitura, e a política de população fica centralizada, testável e uniforme.

O ganho é organizacional: consistência de implementação entre times, chaves e TTL padronizados num único ponto, e a possibilidade de evoluir a estratégia sem tocar em centenas de pontos de leitura. O custo é a dependência de uma camada de abstração que precisa estar bem construída, e a rigidez para casos especiais que fogem do padrão.

No ecossistema Java, o read-through é materializado por bibliotecas consolidadas: loading caches do Caffeine e do Guava, decorators de cache do Spring e implementações de JCache sobre provedores distribuídos. O efeito prático é que a população de cache vira função de loading, e a aplicação só enxerga um repositório de leitura; o mesmo desenho existe em clientes Redis com wrapper de cache para métodos anotados.

O refresh-ahead é a variação proativa: chaves quentes são renovadas antes do vencimento do TTL, por estimativa de acesso. O padrão elimina o miss de pico (o dado nunca expira para o usuário que acessa com frequência) ao custo de renovar dados que talvez ninguém mais use. É o refinamento típico de chaves de dashboards e de top-N de catálogo, onde o miss é visível e o dado é estável.

A implementação honesta do refresh-ahead precisa de limite: sem teto de chaves renovadas por ciclo, o padrão degrada para renovação do cache inteiro, desperdiçando a memória que a carga lazy economizava. A estimativa de acesso costuma ser a contagem de hits na janela anterior, e chaves abaixo do limiar voltam a expirar normalmente.

Escolha por perfil de fluxo de dado

A decisão por fluxo, e não por sistema, se materializa em um mapeamento entre perfil de dado e padrão dominante. O quadro abaixo é o ponto de partida para revisão de arquitetura, e desvios dele precisam de justificativa explícita.

Perfil de fluxo Padrão recomendado Justificativa dominante
Leitura frequente, escrita rara, stale tolerável Cache-aside com TTL Simplicidade máxima e resiliência
Escrita seguida de leitura imediata Write-through Frescor forte observável
Alta taxa de escrita agregável e tolerante a perda Write-behind Latência e consolidação de I/O
Many consumers, política de população única Read-through Uniformidade centralizada
Chave quente estável, miss visível Refresh-ahead sobre cache-aside Elimina miss de pico
Escrita crítica e irreversível Sem cache na escrita Cache não é armazenamento

Comparação por consistência, latência e complexidade

Estratégia Consistência observável Latência de escrita Risco de perda Complexidade de implementação
Cache-aside Eventual, janela de stale em milissegundos a segundos Igual ao banco, invalidação extra Nenhum além do banco Baixa, código explícito na aplicação
Read-through Eventual, controlada pelo provedor Igual ao banco Nenhum além do banco Média, exige camada central
Write-through Forte, cache e banco em passo Alta, dois destinos em série Nenhum além do banco Média, caminho de escrita integrado
Write-behind Eventual, defasagem da fila de flush Baixa, apenas o cache Perda da janela não flushada Alta, fila, flush, alarme e replay
Refresh-ahead Eventual, com renovação proativa Igual à estratégia base Nenhum além da base Média-alta, estimativa de acesso

A leitura de decisão que a tabela organiza: comece por cache-aside com invalidação e TTL, o padrão que cobre a maioria; suba para write-through quando a leitura imediata pós-escrita exige frescor forte; adote write-behind apenas com perda máxima declarada e telemetria de flush; considere read-through quando a uniformidade entre times vale a camada de abstração.

Erros comuns e boas práticas

  • Atualizar o cache na escrita em cache-aside e criar corrida entre commit e SET; invalidar por deleção é o caminho robusto, e a repopulação lazy cuida do resto.
  • Aceitar write-behind em dado crítico sem perceber a janela de perda; se o número é dinheiro, obrigação legal ou estado irreversível, write-through ou escrita direta.
  • Misturar estratégias no mesmo namespace sem documentar qual chave segue qual padrão; a convenção de chave deve codificar o padrão (prefixo wt:, wb:) para diagnóstico rápido.
  • Esquecer o TTL como rede de segurança em todos os padrões; invalidação falha, bug existe, e o TTL curto limita o dano do stale a minutos.
  • Implementar write-through sem fallback quando o cache está fora; a escrita não pode parar porque um acelerador caiu, degradar para o banco é comportamento mínimo.
  • Ignorar chaves frias no write-through; população de dado que ninguém lê gasta memória e eviction, e a política precisa descartar chaves nunca re-lidas.
  • Confiar na ordem de mensagens de invalidação assíncrona; com múltiplos escritores, invalidação fora de ordem repopula stale, e versionamento de chave resolve de forma simples.