02 - Estratégias (Cache-Aside, Write-Through, Write-Behind)
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.