Lab 13 — Cache que salva o banco, e a invalidação que quebra tudo
O problema, e a empresa que o tem
A Cadência é a mesma equipe do L01, do L03 e do L07: dois engenheiros, .NET 8 em ECS Fargate, PostgreSQL no RDS. O L07 resolveu a lentidão do painel gerencial com um índice e uma réplica de leitura. Seis meses depois, a rede cresceu para 1.200 lojas, e um sintoma novo apareceu num endpoint diferente: o catálogo.
Antes de cada venda, o ponto de venda de cada loja consulta o preço e o alerta de estoque do produto pelo SKU. O catálogo tem 40 mil itens, mas o log de acesso mostra que 500 deles — os de giro rápido, os mesmos em quase toda loja — respondem por 80% das chamadas num intervalo de cinco minutos. Nas sextas à tarde, o p95 desse endpoint sobe de 8 ms para 340 ms, e a réplica de leitura do L07 chega a 80% de CPU.
O que torna isso interessante é que a consulta em si está correta: é a mesma que o L07 já indexou. Não é uma consulta ruim rodando devagar — é a consulta CERTA rodando milhares de vezes por minuto para responder exatamente a mesma pergunta. Índice e réplica resolvem consulta lenta; nenhum dos dois resolve consulta repetida.
O que este laboratório NÃO é
Não é otimização de consulta — isso já foi feito no L07, e cachear consulta ruim é pagar hardware para não corrigir o índice; a ordem certa é otimizar primeiro, cachear depois. Também não é cache de sessão de usuário nem cache semântico de prompt de modelo: essa decisão de TTL aplicada a um domínio diferente é o L89, e ela usa exatamente o mecanismo construído aqui.
O que você vai conseguir fazer
Cada objetivo se prova com um comando na seção de implantação, não com a sensação de ter entendido.
- Explicar por que 80% de consultas idênticas continuam caras mesmo com índice e réplica.
- Nomear os três padrões de cache — cache-aside, write-through, write-behind — e o modo de falha de cada um.
- Reproduzir a debandada de cache com TTL fixo e medir o pico que ela causa no banco.
- Explicar por que TTL com jitter e trava de reconstrução eliminam a debandada sem eliminar o cache.
- Diferenciar expiração (TTL) de invalidação explícita, e nomear o bug de leitura concorrente que a invalidação mal ordenada produz.
- Desenhar a chave de cache de forma que ela decida a taxa de acerto, não a fragmente.
- Implementar cache-aside em C#/.NET 8 com trava distribuída via `SET NX`.
- Usar o endpoint primário do ElastiCache corretamente, sabendo que ele é imune a failover.
- Medir p95 antes e depois do cache, com número — não com "ficou mais rápido".
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Cache-aside vs write-through | SAA-C03, DVA-C02 | a aplicação decide o que e quando cachear, o RDS continua sendo a fonte | write-through grava cache e banco juntos; cache-aside só popula no miss |
| Cold cache | SAA-C03 | primeira leitura depois de deploy ou failover sempre bate no banco | que cache vazio não é bug — é o estado inicial esperado |
| TTL e invalidação explícita | DVA-C02, SAA-C03 | TTL com jitter no miss + `DEL` no caminho de escrita | TTL é expiração passiva; invalidação é decisão ativa da aplicação |
| Debandada de cache (thundering herd) | SAA-C03, SOA-C02 | reproduzida de propósito com TTL fixo em teste de carga | jitter e trava de reconstrução como as duas defesas complementares |
| ElastiCache Multi-AZ e endpoints | SAA-C03, DVA-C02 | endpoint primário imune a failover; réplica promovida automaticamente | diferença entre endpoint primário (escrita/leitura) e de leitura (round-robin) |
| Política de evicção | DVA-C02 | `volatile-lru`, o padrão: só expira quem tem TTL | por que toda chave — inclusive a de trava — precisa nascer com TTL |
| Menor privilégio no acesso ao cache | SAA-C03 | AUTH token via Secrets Manager, TLS em trânsito, security group restrito | por que Redis sem AUTH é acesso irrestrito a quem estiver na VPC |
Onde isto costuma ser cobrado errado
A questão clássica descreve um catálogo lido milhares de vezes por minuto e escrito algumas vezes por dia, e pede o padrão de cache de menor custo operacional. Write-through parece "mais correto" porque soa mais consistente, e é a resposta errada: ele paga latência extra em TODA escrita para um dado que raramente muda, só para acelerar uma leitura que o cache-aside já acelera sem tocar no caminho de escrita.
Requisitos, e como cada um muda o desenho
A coluna da direita é a rastreabilidade: toda peça da arquitetura de produção aparece nela. Peça que não aparecesse teria sido retirada antes deste texto existir.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| p95 do catálogo no pico | abaixo de 20 ms | obriga cache na frente da réplica; réplica sozinha, mesmo indexada, não chega lá |
| Tolerância a preço desatualizado, regra geral | até 30 s | vira o TTL base; é a resposta escrita à pergunta "quanto tempo você tolera estar errado" |
| Preço de promoção manual | tem de valer imediatamente | obriga invalidação explícita no caminho de escrita — TTL sozinho não cumpre isso |
| Debandada sob pico de sexta-feira | não pode gerar pico equivalente no banco | obriga TTL com jitter e trava de reconstrução de execução única |
| Sobreviver à queda de um nó de cache | sem derrubar o checkout | obriga Multi-AZ com failover automático no ElastiCache |
| Orçamento do trimestre | não dobra a linha de banco (herdado do L07) | descarta subir a classe da réplica; o nó de cache é a opção mais barata para o mesmo ganho |
| Acesso ao cache | só a aplicação, nunca de fora da VPC | obriga AUTH token via Secrets Manager, TLS em trânsito e security group restrito à task |
| Corretude sob escrita concorrente | nunca servir dado mais velho que o próprio DEL | obriga a ordem commit → invalidação, e o TTL como teto para o caso raro que essa ordem não cobre |
A pergunta que esta tabela inteira responde
Cada linha acima é, no fundo, uma resposta diferente para "quanto tempo você tolera estar errado" — 30 s para o preço em geral, zero para a promoção manual, nunca para a chave que identifica o consumidor errado. Cache não decide isso sozinho; ele só implementa a resposta que o requisito já deu.
Arquitetura mínima: o cache de um nó só
Este desenho acelera de verdade fora do pico, com a menor implementação possível. O laboratório começa medindo o que ele entrega e o que ele esconde — porque um número torna o defeito discutível, e "às vezes fica lento" não.
- → HTTPS 443, GET /catalogo/produtos/{sku}
- → encaminha por número de conexões
- → GET produto:{sku}
- → no miss: a mesma consulta indexada do L07
- → popula com TTL fixo de 30 s, sem jitter
- Fora da AWS
- Rede e entrega
- Compute
- Banco de dados
- Conceito de arquitetura
Este desenho publica e acelera de verdade fora do pico: TTL fixo, um nó, sem trava. O defeito não está em nenhuma linha errada — está em duas ausências que passam despercebidas até 500 chaves expirarem no mesmo segundo. Percorra os passos e repare que a debandada é aritmética, não azar.
- A leitura pergunta primeiro ao cache. Cache-aside: a aplicação, não o banco nem o balanceador, decide consultar o Redis antes de qualquer coisa. Se a chave existe, a réplica nem é acionada — é aqui que 80% das chamadas deveriam parar.
- TTL fixo dá a mesma vida às 500 chaves quentes. As chaves foram aquecidas juntas, no primeiro minuto após o deploy, todas com 30 segundos de validade. Elas não vão morrer em momentos diferentes: vão morrer juntas, porque nasceram juntas e receberam o mesmo prazo.
- Quando o relógio expira, todo mundo erra ao mesmo tempo. No segundo em que o TTL zera, a próxima onda de requisições — que sob pico de sexta-feira pode ser centenas por segundo para o mesmo SKU — encontra a chave ausente simultaneamente. Não há dedo errado aqui: é o resultado matemático de compartilhar um relógio.
- Cada miss dispara a MESMA consulta, N vezes. Sem nada que sirva de fila ou trava, cada uma das N requisições que perderam a chave consulta a réplica de forma independente e idêntica. A réplica que o L07 dimensionou para tráfego distribuído recebe, por um instante, o equivalente a N cópias da mesma pergunta.
- O nó único é ponto único de falha do próprio alívio. Sem Multi-AZ, a queda do único nó de cache não degrada — ela remove o alívio inteiro de uma vez. Toda leitura passa a bater na réplica direto, sem trava e sem cache algum, no pior momento possível para isso acontecer.
- Por que alguém publica assim. Porque funciona, e muito bem, fora da janela de debandada — que em teste manual ninguém reproduz sem querer. É a implementação mais curta que passa em qualquer teste local, e o defeito só aparece sob concorrência real.
Antes de mudar qualquer coisa, meça o ganho real do cache aquecido, e depois provoque a debandada de propósito. Os dois números — o ganho e o pico — são a linha de base que justifica cada peça da arquitetura de produção.
#!/usr/bin/env bash
# medir-p95.sh — mede o p95 do endpoint de catalogo, antes e depois do cache
set -euo pipefail
URL="https://$(terraform output -raw dominio)/api/catalogo/produtos/COD-58231"
# hey: 50 conexoes simultaneas por 30 s, contra UM sku quente. O numero que
# sai daqui e a linha de base — nada de "parece mais rapido".
hey -z 30s -c 50 "$URL" | tee resultado.txt
grep -E "50%|90%|95%|99%" resultado.txt
# Na Cadencia, sem cache: p95 de 340 ms (bate na replica a cada chamada).
# Com cache aquecido: p95 de 4 ms — a diferenca e o que a seção de custo
# compara contra subir a classe da replica para o mesmo ganho.
A debandada não é um bug raro — é aritmética sob pico
Com 500 chaves aquecidas juntas e TTL fixo, o momento da expiração é PREVISÍVEL: é o segundo em que o deploy completou mais 30. No pico de sexta-feira, isso significa que centenas de requisições concorrentes para o MESMO SKU perdem a chave ao mesmo instante e caem, todas, na réplica — o equivalente a apagar o cache na hora de maior tráfego. Se a réplica não aguentar o pico duplicado, o catálogo para para as 1.200 lojas ao mesmo tempo, no horário de maior venda: é dinheiro parado, não só latência alta.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A mudança estrutural não é "adicionar um nó": é a aplicação parar de confiar só no relógio do TTL para tudo.
- → HTTPS 443 sem mudança perceptível
- → encaminha por número de conexões
- → GET produto:{sku} — chave sem id de loja
- → no miss, com trava: a mesma consulta do L07
- → popula com TTL base 30 s + jitter até 10 s
- → replicação assíncrona dentro do cluster
- → grava novo preço, aguarda commit
- → DEL da chave, disparado só após o commit
- → AUTH token, renovado em runtime
- → taxa de acerto, evicção, defasagem da réplica de cache
- Fora da AWS
- Rede e entrega
- Compute
- Banco de dados
- Conceito de arquitetura
- Gestão e governança
- Segurança e identidade
A troca não é "adicionar um nó": é a aplicação parar de confiar só no relógio. O que muda é a existência de uma trava para o momento do miss e de um caminho de escrita que avisa o cache, não só o TTL. Percorra os passos: cada peça nova rastreia a uma linha da tabela de requisitos.
- A chave carrega o que decide a taxa de acerto. A chave é `produto:{sku}`, sem identificador de loja. O preço é o mesmo para as 1.200 lojas — incluir a loja fragmentaria um cache de 500 chaves em até 600 mil combinações quase idênticas, e a taxa de acerto desabaria mesmo sob tráfego alto.
- TTL com jitter espalha a expiração das 500 chaves quentes. Cada chave recebe 30 s mais um valor aleatório de até 10 s, sorteado por chave no momento em que ela é gravada. As 500 deixam de compartilhar um relógio, e a debandada some sem abrir mão do TTL como mecanismo.
- A trava garante que só uma requisição reconstrói. No miss, a aplicação tenta `SET NX` de uma chave de trava com prazo curto. Quem consegue consulta o banco e repovoa; quem não consegue espera um instante e tenta o cache de novo, em vez de repetir a mesma consulta ao banco.
- A escrita grava no banco primeiro, e só então invalida. O `DEL` só é enviado depois de o commit no primário ser confirmado. Inverter essa ordem deixaria uma janela em que o cache já está limpo mas o dado antigo ainda é o único que existe — pior do que não invalidar.
- Falha automática promove a réplica com menor atraso. Com Multi-AZ, a queda do nó primário promove a réplica de cache mais próxima do estado atual. O endpoint primário é um nome de DNS imune a essa troca: a aplicação não muda uma linha de configuração quando isso acontece.
- O painel observa taxa de acerto, não só latência. Latência baixa por si só não prova que o cache funciona — um cache vazio também responde rápido ao encaminhar tudo para a réplica, até ela saturar. A taxa de acerto é o sinal que separa "rápido porque cacheado" de "rápido porque ainda coube".
- A trava não é fila para 1.200 lojas. A trava só existe no instante do miss de UMA chave específica; fora dessa janela estreita, toda leitura segue direto ao Redis sem disputar nada. É uma peça cirúrgica, não um gargalo geral.
O ajuste com maior efeito por linha alterada
Das peças novas, a que mais reduz risco por menos código é o jitter: uma linha que soma um valor aleatório ao TTL elimina a sincronização das 500 chaves quentes sem exigir trava, invalidação nem nó novo. A trava de reconstrução resolve o que sobra — a concorrência DENTRO da janela de miss de uma única chave — e as duas juntas cobrem praticamente toda a debandada medida no teste de carga.
O caminho de uma leitura, e o de uma escrita, ponta a ponta
Os dois caminhos se cruzam num único ponto: a chave `produto:{sku}` no Redis. Entender a ORDEM em que cada um toca essa chave é o que separa uma invalidação que funciona de uma que parece funcionar em teste manual e falha sob concorrência real.
Por que a leitura perdedora não popula o cache
Quando a trava já está com outro processo, a leitura perdedora consulta a réplica diretamente, sem gravar o resultado no Redis. Se ela também gravasse, duas escritas concorrentes na mesma chave poderiam se sobrepor sem nenhuma garantia de qual delas vence — e a resposta certa não é "a última que chegou", é "a que passou pela trava".
O que fica gravado na chave produto:{sku}. "versao" acompanha a coluna do RDS e existe para auditoria — um job periodico pode comparar a versao no cache com a do banco e alertar se elas divergirem por mais tempo que o TTL deveria permitir.
{
"sku": "COD-58231",
"precoCentavos": 129900,
"estoqueAlerta": 12,
"versao": 47,
"cacheadoEm": "2026-08-07T18:03:11Z"
}Repare no que NÃO está no envelope: nenhum campo de loja. A ausência é a decisão da seção de decisões tornada concreta — o mesmo valor serve as 1.200 lojas.
As decisões, e o que se perde em cada uma
📋 Reduzir o p95 do endpoint de catálogo de 340 ms para abaixo de 20 ms no pico de sexta-feira, sem dobrar a linha de banco do orçamento e sem servir preço de promoção manual desatualizado por mais que o tempo de uma escrita.
O cache-aside não pede reescrever o caminho de escrita da aplicação: o RDS continua sendo a única fonte de verdade, e o cache só é consultado e populado pela leitura. Isso combina bem com um sistema onde a leitura é 20 vezes mais frequente que a escrita — o padrão contrário, write-through, pagaria latência de escrita toda vez que um preço muda algumas vezes por dia, para acelerar uma leitura que já é rara em comparação. Um nó de ElastiCache custa uma fração do upgrade de instância de RDS que teria o mesmo efeito no p95, e ataca a causa real: consulta idêntica repetida, não consulta lenta — essa já foi resolvida no L07.
Alt: Write-through — Toda escrita de preço passaria a gravar no cache de forma síncrona antes de confirmar sucesso ao operador. Para um dado que muda algumas vezes por dia e é lido milhares de vezes por minuto, isso adiciona latência ao caminho errado — o raro — para otimizar o caminho que o cache-aside já resolve.
Alt: Write-behind (write-back) — A escrita seria confirmada no cache e aplicada ao RDS depois, de forma assíncrona. Se o nó de cache falhar antes do flush, o preço novo nunca chega ao banco e ninguém percebe até aparecer errado no checkout — perda de dado financeiro que este catálogo não pode aceitar.
Alt: Aumentar a classe da réplica de leitura (alternativa do L07) — Deixa cada consulta mais rápida, mas continua pagando o custo de responder à MESMA pergunta centenas de vezes. É a mesma armadilha de cachear consulta ruim, invertida: aqui a consulta já é boa, o desperdício é a repetição.
Alt: Cache local em memória de cada task — Zero infraestrutura nova e a leitura mais rápida possível — mas cada task tem sua própria cópia, e uma invalidação por `DEL` não alcança todas ao mesmo tempo. O requisito de "promoção vale imediatamente" fica impossível de cumprir de forma coordenada.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Padrão de cache | cache-aside | write-through; write-behind | RDS continua fonte única; menor mudança no caminho de escrita | a primeira leitura depois de um miss ainda paga o custo total (cold read) |
| TTL | 30 s base + jitter de até 10 s | TTL fixo (debandada); TTL longo de 5 min | espalha a expiração das chaves quentes e ainda cobre a tolerância declarada | leitura entre o commit e a invalidação pode servir até o TTL de valor velho, se o `DEL` falhar |
| Invalidação | `DEL` após o commit, disparado pela escrita | `SET` com o valor novo direto; TTL curto sem invalidação nenhuma | evita gravar formato diferente do que a leitura padrão produz; força releitura real | mais uma chamada de rede por escrita |
| Trava de reconstrução | `SET NX` com expiração curta | sem trava (aceitar a debandada); fila externa via SQS | resolve com uma chamada, sem infraestrutura nova | quem não pegou a trava espera; a trava tem TTL próprio para não travar por processo morto |
| Topologia do cache | grupo de replicação Multi-AZ, 2 nós | 1 nó (desenho mínimo); cluster mode enabled (sharding) | falha de nó não derruba o catálogo para 1.200 lojas de uma vez | dobra o custo de nó em relação a 1 só; sharding seria complexidade sem necessidade no volume atual |
| Chave de cache | apenas o SKU, global | SKU + identificador de loja | o dado é idêntico para as 1.200 lojas; incluir a loja fragmentaria em até 600 mil combinações | nenhum aqui — só mudaria se o preço variasse por loja, o que não é o caso |
Construir: o grupo de replicação Multi-AZ
A diferença entre este recurso e um `aws_elasticache_cluster` avulso é exatamente o que o requisito de disponibilidade pede: `automatic_failover_enabled` e `multi_az_enabled` só existem em grupo de replicação.
# elasticache.tf — o grupo de replicacao Multi-AZ, e por que nao e um no so
resource "aws_elasticache_subnet_group" "cache" {
name = "${var.projeto}-cache"
subnet_ids = aws_subnet.privada[*].id
}
resource "aws_elasticache_parameter_group" "cache" {
name = "${var.projeto}-redis7"
family = "redis7"
# O padrao da AWS ja e volatile-lru: sob pressao de memoria, so expira quem
# TEM ttl. Chave sem ttl nunca sai por essa politica — e e por isso que toda
# chave deste laboratorio nasce com ttl explicito, inclusive a de trava.
parameter {
name = "maxmemory-policy"
value = "volatile-lru"
}
}
resource "aws_elasticache_replication_group" "cache" {
replication_group_id = "${var.projeto}-catalogo"
description = "Cache de catalogo: cache-aside com invalidacao pela escrita"
engine = "redis"
engine_version = "7.1"
node_type = "cache.r7g.large"
# 1 primario + 1 replica: e o minimo que autoriza failover automatico. Um
# unico no (num_cache_clusters = 1) e o desenho MINIMO deste laboratorio, e
# o motivo de nao ir para producao esta na secao de decisoes.
num_cache_clusters = 2
automatic_failover_enabled = true
multi_az_enabled = true
parameter_group_name = aws_elasticache_parameter_group.cache.name
subnet_group_name = aws_elasticache_subnet_group.cache.name
security_group_ids = [aws_security_group.cache.id]
# AUTH exige criptografia em transito habilitada; sem os dois juntos o
# provider recusa o apply. E o par que fecha a exposicao da secao de
# seguranca.
at_rest_encryption_enabled = true
transit_encryption_enabled = true
auth_token = data.aws_secretsmanager_secret_version.cache_auth.secret_string
# Snapshot aqui NAO e backup de dado de negocio — o RDS continua sendo a
# unica fonte de verdade. Ele so acelera o reaquecimento apos substituicao
# de no, e por isso um dia de retencao basta.
snapshot_retention_limit = 1
tags = { Projeto = var.projeto }
}
resource "aws_security_group" "cache" {
name = "${var.projeto}-cache"
vpc_id = aws_vpc.principal.id
# So a task fala com o cache. Nao ha 0.0.0.0/0 nem faixa de sub-rede: a
# origem permitida e o security group DA TASK, por referencia.
ingress {
from_port = 6379
to_port = 6379
protocol = "tcp"
security_groups = [aws_security_group.task.id]
}
}
# Leitura do segredo com recurso especifico — nao ha "*" nesta politica.
data "aws_iam_policy_document" "ler_segredo_cache" {
statement {
effect = "Allow"
actions = ["secretsmanager:GetSecretValue"]
resources = [aws_secretsmanager_secret.cache_auth.arn]
}
}
output "endpoint_primario_cache" {
value = aws_elasticache_replication_group.cache.primary_endpoint_address
description = "DNS que sempre resolve para o no primario — imune a failover"
}
Redis sem AUTH e sem TLS é acesso irrestrito dentro da VPC
Sem `transit_encryption_enabled` e `auth_token`, qualquer processo que alcance a porta 6379 na mesma VPC lê e sobrescreve o catálogo inteiro sem autenticação nenhuma — preço, estoque, tudo. Os dois parâmetros são exigidos juntos pelo provedor: AUTH depende de criptografia em trânsito habilitada. Isso não é endurecimento opcional; é a diferença entre "cache da aplicação" e "banco de dados sem senha na rede interna".
Por que o Terraform expõe `primary_endpoint_address`, e não o IP de um nó
Usar o endereço de um nó específico funciona no dia do deploy e quebra no primeiro failover, porque aquele nó pode deixar de ser o primário. O `primary_endpoint_address` é um nome de DNS mantido pela AWS que sempre aponta para quem estiver no papel de primário — o mesmo princípio do endpoint fixo que o L07 já usava para o RDS.
Construir: cache-aside com trava, em C#
A trava é a peça que faz a diferença entre o desenho mínimo e o de produção sob concorrência. Ela só protege o INSTANTE do miss — fora dele, toda leitura vai direto ao Redis sem disputar nada.
// ProdutoCacheService.cs — cache-aside com trava de reconstrucao e invalidacao pela escrita
public sealed class ProdutoCacheService
{
private readonly IConnectionMultiplexer _redis;
private readonly IProdutoRepository _repo; // consulta a REPLICA do L07, nao o primario
private static readonly Random _jitter = Random.Shared;
private const int TtlBaseSegundos = 30; // o numero que o negocio aceitou tolerar
private const int JitterMaximoSegundos = 10; // espalha a expiracao das chaves quentes
private const int TravaTimeoutSegundos = 5; // teto: mesmo se o titular cair, a trava solta
public ProdutoCacheService(IConnectionMultiplexer redis, IProdutoRepository repo)
{
_redis = redis;
_repo = repo;
}
public async Task<ProdutoCacheDto?> ObterAsync(string sku)
{
var db = _redis.GetDatabase();
// A chave NAO leva loja: o preco e o mesmo para as 1.200 lojas, e
// incluir a loja fragmentaria um cache de 500 chaves em ate 600 mil.
var chave = $"produto:{sku}";
var cacheado = await db.StringGetAsync(chave);
if (cacheado.HasValue)
return JsonSerializer.Deserialize<ProdutoCacheDto>(cacheado!);
// MISS. So uma requisicao reconstroi; as outras aguardam um instante
// ou seguem direto ao banco SEM popular — a trava evita a debandada,
// nao o miss em si, que e inevitavel na primeira leitura de cada chave.
var chaveTrava = $"trava:{chave}";
var obteveTrava = await db.StringSetAsync(
chaveTrava, Environment.MachineName,
TimeSpan.FromSeconds(TravaTimeoutSegundos), When.NotExists);
if (!obteveTrava)
{
await Task.Delay(150); // quem tem a trava provavelmente ja publicou
var segundaTentativa = await db.StringGetAsync(chave);
if (segundaTentativa.HasValue)
return JsonSerializer.Deserialize<ProdutoCacheDto>(segundaTentativa!);
// Ainda sem cache: consulta a replica SEM popular, para nao
// competir por escrita no Redis com quem tem a trava.
return await ConsultarReplicaAsync(sku, populaCache: false);
}
try
{
return await ConsultarReplicaAsync(sku, populaCache: true);
}
finally
{
await db.KeyDeleteAsync(chaveTrava);
}
}
private async Task<ProdutoCacheDto?> ConsultarReplicaAsync(string sku, bool populaCache)
{
var produto = await _repo.BuscarPorSkuAsync(sku); // a mesma consulta indexada do L07
if (produto is null) return null;
if (populaCache)
{
var ttl = TimeSpan.FromSeconds(TtlBaseSegundos + _jitter.Next(0, JitterMaximoSegundos));
var db = _redis.GetDatabase();
await db.StringSetAsync($"produto:{sku}", JsonSerializer.Serialize(produto), ttl);
}
return produto;
}
public async Task InvalidarAposEscritaAsync(string sku)
{
// Chamado SO depois do commit no primario ser confirmado. DEL, nunca
// SET com o valor novo: gravar aqui arriscaria formato diferente do
// que a leitura padrao produz, e o proximo GET reconstroi certo.
var db = _redis.GetDatabase();
await db.KeyDeleteAsync($"produto:{sku}");
}
}
A trava tem TTL próprio, e isso não é redundância
Se o processo que segurou a trava morrer no meio da reconstrução — deploy, OOM, qualquer motivo — uma trava sem prazo ficaria presa para sempre, e nenhuma requisição conseguiria repovoar aquela chave. Os 5 s de `TravaTimeoutSegundos` são o teto de dano de uma falha desse tipo: pior caso, a chave fica sem repovoar por 5 s a mais, não para sempre.
Construir: a escrita, e a ordem que decide a corrida
O `AtualizarPrecoAsync` é o outro lado da chave: quem grava o preço também é quem avisa o cache, e a ordem entre as duas ações não é um detalhe de estilo.
// PrecoService.cs — a ordem que importa: commit primeiro, invalidacao depois
public sealed class PrecoService
{
private readonly AppDb _db;
private readonly ProdutoCacheService _cache;
public PrecoService(AppDb db, ProdutoCacheService cache)
{
_db = db;
_cache = cache;
}
public async Task AtualizarPrecoAsync(string sku, decimal novoPreco)
{
await using var tx = await _db.Database.BeginTransactionAsync();
var produto = await _db.Produtos.SingleAsync(p => p.Sku == sku);
produto.PrecoCentavos = (int)(novoPreco * 100);
produto.Versao++;
await _db.SaveChangesAsync();
await tx.CommitAsync(); // so invalida DEPOIS do commit confirmado no primario
// Inverter esta ordem — invalidar antes do commit — abriria uma
// janela em que o cache ja esta vazio mas o dado antigo ainda e o
// UNICO que existe: pior do que nao ter invalidado nada.
await _cache.InvalidarAposEscritaAsync(sku);
}
}
# Este laboratorio reaproveita a imagem do L03. A unica dependencia nova e o
# cliente StackExchange.Redis, que entra no .csproj:
#
# <PackageReference Include="StackExchange.Redis" Version="2.8.16" />
#
# Nao ha estagio de build novo nem porta nova exposta.
O bug que sobrevive à ordem certa: invalidar não é o mesmo que garantir
Mesmo com o commit ANTES do `DEL`, uma leitura que já estava em voo para a réplica de leitura do L07 antes da escrita pode terminar DEPOIS do `DEL` — porque a réplica é assíncrona e pode não ter alcançado o commit ainda. Essa leitura grava o valor ANTIGO no cache, agora sem nenhuma trava impedindo. É o bug de "apaguei e uma leitura concorrente repopulou com o valor velho", e ele custa dinheiro quando o valor é preço: cliente paga menos que o preço correto até o TTL expirar. A ordem certa do código não elimina essa corrida — ela só garante que o pior caso é limitado pelo TTL, nunca indefinido. Para a promoção manual, que precisa valer no segundo seguinte, a rotina do operador confirma que a chave está ausente do Redis antes de anunciar sucesso — não confia só em o `DEL` ter sido enviado.
Implantar, e provar com número
Cinco provas. Nenhuma aceita "ficou mais rápido" como resultado — cada uma tem um número ou um estado esperado, e a segunda é a que mais gente pula por parecer destrutiva demais para rodar.
#!/usr/bin/env bash
# provocar-debandada.sh — reproduz a debandada de proposito, no desenho MINIMO
set -euo pipefail
# 1. Aquece as 500 chaves quentes de uma vez, TODAS com o MESMO ttl fixo —
# e exatamente o que o desenho minimo faz no deploy.
for sku in $(cat skus-quentes.txt); do
curl -s "https://$(terraform output -raw dominio)/api/catalogo/produtos/$sku" >/dev/null
done
echo "500 chaves aquecidas juntas, ttl fixo de 30s"
# 2. Sustenta carga por mais de 30 s. Observe o CloudWatch da REPLICA durante
# a janela em torno do segundo 30: DatabaseConnections deve saltar.
hey -z 90s -c 100 -m GET \
$(sed 's|^|https://'"$(terraform output -raw dominio)"'/api/catalogo/produtos/|' skus-quentes.txt) \
2>&1 | tail -5
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS --metric-name DatabaseConnections \
--dimensions Name=DBInstanceIdentifier,Value=ffv-lab-replica \
--start-time "$(date -u -d '-2 min' +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
--period 10 --statistics Maximum --output table
# Esperado: um pico isolado por volta do segundo 30, e nao antes disso —
# a assinatura de uma debandada sincronizada, nao de carga organica.
# provas.sh — cinco medicoes; nenhuma conclusao vem de "parece que melhorou"
PROJETO=ffv-lab; SKU=COD-58231
# ── Prova 1: p95 antes e depois do cache aquecido ────────────────────────────
# Rode medir-p95.sh com o cache frio (recem-implantado) e de novo com o
# cache quente. Esperado: p95 cai de ~340 ms para a casa de um digito.
# ── Prova 2: a debandada acontece no MINIMO e some na PRODUCAO ───────────────
# provocar-debandada.sh no desenho minimo mostra pico isolado de
# DatabaseConnections no ciclo do ttl. O MESMO script contra o desenho de
# producao (ttl com jitter + trava) nao deve mostrar pico correlacionado —
# a leitura de DatabaseConnections deve ficar estavel durante a janela.
# ── Prova 3: a trava reduz N consultas identicas para 1 ──────────────────────
psql "$DATABASE_URL_REPLICA" -c \
"SELECT calls FROM pg_stat_statements WHERE query LIKE '%produtos WHERE sku%'"
# Anote o valor de 'calls'. Provoque um miss simultaneo de 50 requisicoes na
# MESMA chave (ab -n 50 -c 50). Sem trava, calls sobe por volta de 50; com
# trava, sobe por volta de 1 — as outras 49 aguardaram ou pularam a populacao.
# ── Prova 4: a invalidacao vale no proximo GET, nao no proximo TTL ───────────
curl -s "https://.../api/catalogo/produtos/$SKU" | jq .preco_centavos # antes
curl -s -X PUT "https://.../api/catalogo/produtos/$SKU/preco" \
-d '{"precoCentavos": 99900}' # muda
curl -s "https://.../api/catalogo/produtos/$SKU" | jq .preco_centavos # depois
# Esperado: o terceiro comando ja devolve 99900, sem esperar os 30 s de ttl.
# ── Prova 5: failover do cache, medido pela aplicacao ─────────────────────────
aws elasticache test-failover \
--replication-group-id "${PROJETO}-catalogo" --node-group-id 0001
( while true; do
curl -s -o /dev/null -w '%{http_code} ' "https://.../api/catalogo/produtos/$SKU"
sleep 0.2
done ) & LACO=$!
sleep 60; kill $LACO
# Esperado: nenhum erro de conexao — a aplicacao usa o endpoint primario, que
# e o mesmo DNS antes e depois. Pode haver um breve aumento de latencia (cold
# miss na chave recem-promovida), nao erro.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · p95 antes/depois do cache | `medir-p95.sh` com cache frio e depois quente | p95 cai de ~340 ms para a casa de um dígito | ganho pequeno indica taxa de acerto baixa — confira fragmentação de chave |
| 2 · debandada some na produção | `provocar-debandada.sh` no mínimo e na produção | pico isolado de `DatabaseConnections` some no desenho com jitter + trava | se o pico continuar na produção, o jitter não está sendo aplicado ou a trava falhou |
| 3 · trava reduz N consultas para 1 | `pg_stat_statements` antes/depois de miss simultâneo | `calls` sobe por volta de 1, não por volta de N | se sobe por volta de N, a trava não está impedindo reconstrução concorrente |
| 4 · invalidação vale no próximo GET | GET, PUT de preço, GET imediato | o terceiro GET já devolve o preço novo, sem esperar o TTL | se ainda devolve o antigo, o `DEL` não foi disparado ou foi enviado antes do commit |
| 5 · failover sem erro de aplicação | `test-failover` mais laço de `curl` durante 60 s | nenhum código de erro; possível aumento breve de latência | erro de conexão indica que a aplicação está usando o endpoint fixo do nó, não o primário |
Quebrar de propósito: três falhas e o diagnóstico
As três produzem sintomas parecidos à primeira vista — "o catálogo está lento de novo" — e o que separa é onde se olha primeiro.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Debandada de propósito | remova o jitter e a trava; aqueça as 500 chaves juntas com TTL fixo de 30 s | pico periódico de `DatabaseConnections` na réplica, a cada 30 s exatos | `CloudWatch` correlacionando o período do pico ao valor do TTL configurado | reintroduzir jitter por chave e `SET NX` como trava de reconstrução |
| Chave fragmentada por loja | inclua o identificador da loja na chave: `produto:{loja}:{sku}` | taxa de acerto despenca mesmo sob tráfego alto; a réplica volta a receber quase tudo | `SCAN` contando a cardinalidade real de chaves — muito maior que os 500 SKUs esperados | remover o campo que fragmenta a chave; ela deve refletir o dado, não o consumidor |
| Trava sem TTL | troque `SET NX EX 5` por `SET NX` sem prazo | um crash no titular da trava trava a reconstrução daquela chave para sempre | `TTL trava:produto:SKU` retornando `-1` no Redis | sempre definir expiração explícita na trava, nunca depender de remoção manual |
A métrica que engana durante o teste de debandada
Latência média pode continuar baixa mesmo com a debandada acontecendo, porque ela dura poucos segundos dentro de uma janela de 30. Olhar só a média esconde o pico; o p99 e a contagem de conexões do banco no segundo exato da expiração é que denunciam o padrão.
O desenho mínimo aquece 500 chaves quentes de uma vez, todas com TTL fixo de 30 s. Sob o pico de sexta-feira, o que acontece quando o TTL expira?
Segurança: o cache é uma cópia do catálogo, e precisa ser tratado como tal
Um nó de cache sem controle de acesso não é menos sensível que o banco: ele guarda preço e estoque de todo o catálogo, em texto legível, para quem alcançar a porta.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Redis exposto sem AUTH/TLS na VPC | média | alto | AUTH token via Secrets Manager + `transit_encryption_enabled` | VPC Flow Logs com conexão à porta 6379 fora do security group da task | rotacionar o AUTH token e revisar o security group imediatamente |
| AUTH token em log ou variável de ambiente | baixa | alto | token lido do Secrets Manager em runtime, nunca embutido em imagem nem log | busca por padrão de token nos grupos de log da aplicação | rotacionar o token e invalidar conexões abertas com o valor antigo |
| Debandada derruba a réplica no pico de vendas | média | alto | TTL com jitter e trava de reconstrução, testados sob carga antes do deploy | alarme de `DatabaseConnections`/CPU correlacionado ao ciclo do TTL | aumentar o TTL de emergência via parâmetro e investigar depois com calma |
| Preço errado servido por corrida de invalidação | baixa | alto | ordem commit → `DEL`; TTL como teto de exposição, nunca sem limite | job periódico comparando a versão cacheada com a versão do RDS | apagar a chave manualmente e checar o log de escrita daquele SKU |
| Chave sem TTL sob política `volatile-lru` | baixa | médio | toda chave — dado e trava — nasce com TTL explícito no código | uso de memória do Redis crescendo sem evicção correspondente | corrigir o caminho que grava sem TTL e forçar expiração das chaves órfãs |
Observabilidade: as perguntas que o painel tem de responder
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| O cache está sendo usado de verdade? | taxa de acerto (`GetTypeCmds` vs `GetMissCmds`) | queda é chave fragmentada ou TTL curto demais para o padrão de acesso | < 90% em horário comercial |
| O nó está saturado de memória? | `DatabaseMemoryUsagePercentage` | sob `volatile-lru`, evicção começa a apagar chave viva antes do TTL | > 80% |
| Há debandada em andamento? | `DatabaseConnections` da réplica correlacionado ao ciclo do TTL | pico periódico no mesmo intervalo do TTL é expiração sincronizada, não carga orgânica | pico > 3× a média fora do ciclo |
| A réplica de cache está defasada? | `ReplicationLag` do grupo de replicação | leitura logo após um failover pode devolver dado mais velho que o esperado | > 1 s |
| Alguém conectou fora da aplicação? | VPC Flow Logs na porta 6379 | acesso fora do security group da task é o primeiro risco da tabela de segurança | qualquer origem fora do SG esperado |
| A trava está vazando? | contagem de chaves `trava:*` fora do caminho quente, via `SCAN` | trava sem TTL nunca libera, e a reconstrução daquela chave para de acontecer | qualquer chave de trava com TTL `-1` |
| Toda escrita de preço disparou invalidação? | log de escrita comparado à taxa de acerto no minuto seguinte | invalidação falhando silenciosamente serve preço velho sem nenhum alarme óbvio | qualquer escrita sem `DEL` correspondente |
Nunca use `KEYS` em produção para essas medições
`KEYS *` varre o espaço inteiro de chaves de forma bloqueante, num Redis de thread única — e trava as outras milhares de chamadas por segundo enquanto varre. `SCAN` itera em lotes pequenos sem bloquear, e é o comando certo para contar cardinalidade ou achar chave órfã, inclusive em produção sob carga.
Taxa de acerto tem duas fontes, e elas às vezes discordam
O comando `INFO stats` do próprio Redis expõe `keyspace_hits` e `keyspace_misses` acumulados desde o último restart do nó; as métricas do CloudWatch (`GetTypeCmds`, `GetMisses`) são por período e sobrevivem a um failover. Para decidir se a taxa de acerto piorou HOJE, use o CloudWatch — o `INFO stats` mistura o histórico inteiro do nó.
Escala: 10, 10 mil, 1 milhão, e falha de AZ
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 req/s | 1 nó sem Multi-AZ já resolve; debandada não chega a aparecer | nada; é o cenário do desenho mínimo em ambiente de teste | nada |
| 3 mil req/s (pico atual) | 2 nós Multi-AZ, TTL com jitter, trava de reconstrução | é exatamente o desenho de produção deste laboratório | nada além do já construído |
| 30 mil req/s | o throughput de COMANDOS do nó vira o teto antes da memória | latência sobe mesmo com taxa de acerto alta, porque o nó satura em requisições/s | considerar `cluster mode enabled` (sharding) — fora do escopo deste laboratório |
| 1 milhão de chamadas/s (catálogo público futuro) | um nó só não sustenta, mesmo shardeado sem cuidado | chave mal desenhada concentra tráfego num único shard (hot shard) | reprojetar a chave para distribuir hash, e avaliar cache de borda para o catálogo público — é o L17 |
| Falha de AZ durante o pico | failover automático promove a réplica com menor atraso | a aplicação não muda nada, porque usa o endpoint primário; há janela breve de cold miss | a prova 5 desta seção mede exatamente essa janela |
O gargalo que a memória não avisa
Um nó de ElastiCache pode estar com memória sobrando e ainda assim saturar, porque o limite de comandos por segundo é separado do limite de memória. Dimensionar só pela quantidade de dado cacheado — sem olhar o throughput medido — é o erro mais comum ao planejar escala de cache.
Custo: o que este laboratório acrescenta à fatura
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | 1 nó, sem Multi-AZ | um nó pequeno ligado durante o teste | desprezível | nenhuma; otimizar aqui é atenção gasta onde não há dinheiro em jogo |
| Produção pequena (2 nós Multi-AZ) | 3 mil req/s no pico | 2 nós ligados 24/7, cobrando por hora independente de tráfego | previsível e fixa | dimensionar o tipo de nó pelo throughput de comandos medido, não pela intuição |
| Alta escala (sharded) | 30 mil+ req/s | múltiplos nós de partição, cada um com sua réplica | cresce com o número de shards, não com o volume lido do cache | cache continua muito mais barato que escalar RDS pelo mesmo ganho de leitura |
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Nó do ElastiCache | hora ligada, por tipo de nó, independente de tráfego | ao contrário do RDS sob carga variável, o cache cobra fixo — dimensione pelo pico |
| Transferência entre AZ | GB entre nó primário e réplica em AZs diferentes | tráfego de replicação interna do Multi-AZ soma, mesmo sendo pequeno por natureza |
| Snapshot do cache | GB-mês armazenado | não é backup de negócio — existe só para acelerar substituição de nó |
| Réplica de RDS herdada do L07 | continua cobrando igual, agora atendendo menos | o cache reduz a CARGA na réplica; não elimina a necessidade dela |
A comparação que justifica o laboratório
Um nó de ElastiCache suficiente para o pico atual custa uma fração do upgrade de classe de réplica que teria o mesmo efeito no p95. E o cache ataca a causa real — volume de consulta idêntica repetida — enquanto uma réplica maior só deixaria cada cópia da mesma pergunta um pouco mais rápida.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | cache-aside com trava, invalidação pela escrita, painel de taxa de acerto | auditoria de divergência entre cache e banco ainda é manual | job periódico de reconciliação entre cache e RDS | média |
| Segurança | AUTH + TLS, security group restrito à task, segredo via Secrets Manager | acesso de dentro da própria VPC continua sendo superfície de ataque | avaliar RBAC do Redis (grupos de usuário) em vez de token único | média |
| Confiabilidade | Multi-AZ com failover automático; TTL como teto de degradação sob qualquer corrida | cold cache logo após failover gera pico breve de leitura na réplica | pré-aquecer as chaves mais quentes assim que a promoção termina | média |
| Eficiência de performance | p95 sub-20 ms no pico atual; chave desenhada para taxa de acerto alta | um nó só pode saturar throughput de comandos antes de saturar memória | `cluster mode enabled` quando o throughput medido exigir | baixa |
| Otimização de custos | nó fixo mais barato que upgrade de réplica para o mesmo ganho de leitura | 2 nós custam igual fora do pico, quando o tráfego é bem menor | avaliar redução programada de nó fora do horário de pico, se a AWS permitir | baixa |
| Sustentabilidade | menos consultas idênticas repetidas significa menos ciclos de CPU no banco | armazenamento em memória permanece ligado mesmo ocioso fora do pico | nenhuma ação imediata; o volume atual justifica o custo fixo | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho novo: é a resposta a QUANDO o cache atual para de bastar. Cada nível resolve um risco e compra outro.
Sem cache: toda leitura bate direto na réplica do L07. Era o estado da Cadência antes deste laboratório.Cache-aside com TTL e jitter, trava de reconstrução, invalidação pela escrita, ElastiCache Multi-AZ.`cluster mode enabled`, múltiplas partições, cada uma com sua própria capacidade de comandos por segundo.CDN ou similar na frente do catálogo público, fora da VPC, mais perto da loja fisicamente distante da região.Vários times compartilhando o mesmo cluster de cache, com namespace de chave por domínio e cota de memória por time.Cache semântico para resposta de modelo, com TTL decidido pela confiança da resposta em vez de tempo fixo — o assunto do L89.A ordem não é negociável, e o motivo é concreto
Sharding no nível 3 sem jitter e trava do nível 2 só distribui a debandada entre mais nós — cada partição sincroniza sua própria fatia de chaves e sofre o mesmo pico, em escala menor mas no mesmo instante. Quem pula o nível 2 leva o defeito consigo para o 3.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não decide TTL nem substitui a trava de reconstrução — as duas são aritmética e política de negócio escritas em código, e um modelo não melhora nenhuma das duas. Forçar IA aqui seria o antipadrão que a própria série critica.
Há um lugar modesto onde IA acrescentaria valor: decidir quais das 40 mil SKUs pré-aquecer antes do pico de sexta, em vez de deixar o primeiro pedido de cada chave pagar o custo total de um cold read.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | escolher quais SKUs pré-aquecer antes do pico, além dos 500 mais vendidos óbvios, capturando sazonalidade que uma regra fixa não vê |
| Por que uma regra não bastaria? | uma regra simples — "os 500 mais vendidos dos últimos 7 dias" — já cobre os 80% medidos; IA só se justificaria para os 20% restantes, que hoje não é onde o p95 do módulo está sendo medido |
| De onde viriam os dados? | histórico de vendas por SKU, calendário de promoções, e a própria taxa de acerto por chave já coletada pelo CloudWatch |
| Qual o risco? | pré-aquecer SKU errado desperdiça memória do nó, e sob `volatile-lru` isso pressiona a evicção de chave que de fato importa |
| Por que não agora? | a regra dos 500 mais vendidos já cobre a demanda medida; IA aqui otimizaria a margem, não o problema que motivou o laboratório |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para decidir o TTL de cada chave dinamicamente troca uma política auditável — um número que a área de negócio aprovou e que qualquer engenheiro lê no código — por uma decisão opaca que ninguém consegue explicar quando o preço errado aparece no checkout. TTL é política de negócio registrada, não previsão estatística.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Cache-aside sem trava de reconstrução | funciona perfeitamente fora do pico e é a implementação mais curta | debandada exatamente quando o sistema mais precisa do cache funcionando | pico periódico de conexões no banco no mesmo intervalo do TTL | `SET NX` com TTL curto como trava de execução única | tráfego baixo o suficiente para nunca haver miss concorrente na mesma chave |
| Identificador de loja na chave de dado compartilhado | parece mais seguro, "cada loja no seu canto" | fragmenta um cache de 500 chaves em até 600 mil combinações quase idênticas | taxa de acerto baixa mesmo sob tráfego alto e concentrado nos mesmos SKUs | chave pelo identificador do DADO, não do consumidor | quando o dado realmente varia por loja — por exemplo, estoque local |
| `SET` com valor novo em vez de `DEL` na invalidação | parece mais eficiente, "já deixa pronto para a próxima leitura" | risco de gravar formato diferente do que a leitura padrão produziria, sem nenhuma garantia de que é o valor mais recente sob escrita concorrente | cache com formato inconsistente aparecendo de forma esporádica e difícil de reproduzir | `DEL` e deixar o próximo `GET` repovoar pela rota já testada | quando existe só um escritor e o valor é trivial de serializar |
| TTL igual para toda chave, sem jitter | é a primeira linha que funciona e todo tutorial faz assim | sincroniza a expiração das chaves quentes e produz debandada sob carga real | pico em formato de dente de serra nas métricas de conexão do banco | TTL base mais jitter aleatório sorteado por chave | tráfego baixo demais para haver miss concorrente relevante |
| Cache local em memória de cada processo | zero infraestrutura nova, e é a leitura mais rápida que existe | cada task tem sua própria cópia, e invalidação nunca alcança todas ao mesmo tempo | preço desatualizado aparece em algumas lojas e não em outras, dependendo de qual task atendeu | cache compartilhado (Redis) para dado que precisa de invalidação coordenada | dado verdadeiramente imutável durante a vida do processo |
| Cachear a consulta lenta em vez de corrigi-la | é mais rápido de implementar do que investigar o plano de execução | paga hardware de cache para esconder o defeito em vez de removê-lo — e toda chave nova ainda sofre o custo total no primeiro acesso | p95 melhora no painel, mas o plano de execução continua sendo uma varredura cara | otimizar a consulta primeiro (L07), cachear depois | nunca — é a ordem que este laboratório assume como pré-requisito |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Pico periódico de conexões no banco, sempre no mesmo intervalo | TTL sincronizado, sem jitter | correlacione o período do pico com o valor do TTL configurado | CloudWatch `DatabaseConnections` vs. tempo | adicionar jitter por chave e trava de reconstrução |
| Taxa de acerto baixa mesmo sob tráfego alto | chave fragmentada por um campo que não deveria estar nela | conte a cardinalidade real de chaves com `SCAN` | padrão de chave no Redis vs. número esperado de SKUs | remover o campo que fragmenta a chave |
| Preço antigo aparece minutos depois da alteração | invalidação não disparou, ou falhou silenciosamente | confira se o `DEL` foi de fato chamado no log da aplicação | log de escrita vs. TTL restante da chave (`TTL produto:SKU`) | garantir que a invalidação está no mesmo caminho do commit, com tratamento de erro |
| Aplicação trava esperando o cache no miss | trava de reconstrução sem TTL, vazou | confira o TTL da chave de trava | `TTL trava:produto:SKU` retornando `-1` | sempre definir expiração explícita na trava |
| Erro de autenticação depois de rotacionar o token | AUTH token rotacionado no Secrets Manager sem reconectar o cliente | confira a versão do segredo que a aplicação está lendo | logs de conexão do cliente Redis | reconectar com backoff ao detectar erro de auth, com sobreposição de versão do segredo |
| Falha de nó não gerou failover | Multi-AZ ou failover automático desligado no grupo de replicação | confira a configuração do grupo | `describe-replication-groups` → `AutomaticFailover` | habilitar `automatic_failover_enabled` e `multi_az_enabled` |
A pergunta que resolve metade destes casos
Antes de mexer em qualquer parâmetro, pergunte: o problema é de ACERTO (taxa de hit) ou de CORRETUDE (dado errado)? Taxa de acerto baixa aponta para chave ou TTL; dado errado aponta para invalidação. As duas famílias de sintoma se parecem no relatório do usuário — "o catálogo está errado" — e apontam para investigações completamente diferentes.
Limpeza: o que o destroy não leva
O ElastiCache tem dois comportamentos de saída que surpreendem: um snapshot final pode sobreviver ao `destroy`, e o segredo no Secrets Manager entra numa janela de recuperação antes de sumir de verdade.
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# 2. SNAPSHOT FINAL: se "final_snapshot_identifier" estava setado, ele
# sobrevive ao destroy e cobra GB-mes sozinho.
aws elasticache describe-snapshots --replication-group-id ffv-lab-catalogo \
--query "Snapshots[].SnapshotName" --output table
aws elasticache delete-snapshot --snapshot-name <nome-do-snapshot> 2>/dev/null || true
# 3. SEGREDO NO SECRETS MANAGER: por padrao entra em janela de recuperacao
# de 30 dias, continuando a cobrar taxa mensal ate a exclusao definitiva.
aws secretsmanager delete-secret \
--secret-id ffv-lab-cache-auth --force-delete-without-recovery
# 4. Confirme: nada com o nome do projeto de pe.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab \
--query "ResourceTagMappingList[].ResourceARN" --output table| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Grupo de replicação ElastiCache | sim | não | os dois nós saem junto com o grupo |
| Snapshot final do cache | não, se identificador foi setado | sim, GB-mês | existe justamente para sobreviver ao destroy; precisa ser apagado à parte |
| Segredo no Secrets Manager | entra em espera, não some | sim, taxa mensal | janela de recuperação padrão de 30 dias antes da exclusão real |
| Security group do cache | sim | não | nenhuma pendência |
| Réplica de RDS e demais recursos do L07 | sim | sim, por hora | não fazem parte deste laboratório; confira a limpeza do L07 separadamente |
O snapshot final é a pendência que ninguém procura
Ele não aparece na lista de recursos do Terraform depois do `destroy`, porque nunca foi gerenciado por ele — é um efeito colateral do provedor. Quem interrompe o laboratório sem rodar o passo 2 da limpeza acima descobre esse custo meses depois, numa fatura sem nenhum recurso "vivo" visível no console para explicá-lo.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| 80% das consultas idênticas batendo na réplica | cache-aside no ElastiCache | RDS continua fonte única; menor mudança no caminho de escrita existente |
| 500 chaves expirando no mesmo segundo | TTL base + jitter por chave | espalha a expiração sem abrir mão do TTL como mecanismo de tolerância |
| N requisições reconstruindo a mesma chave ao mesmo tempo | trava `SET NX` no miss | só uma reconstrói; as outras esperam ou seguem sem popular |
| Preço de promoção precisando valer imediatamente | `DEL` disparado pela escrita, após o commit | invalidação explícita cobre o que o TTL sozinho não cumpre |
| Nó de cache como ponto único de falha | grupo de replicação Multi-AZ | failover automático promove a réplica com menor atraso, sem mudar o endpoint que a app usa |
| Cache fragmentado por loja | chave só com o SKU, sem identificador de consumidor | a chave reflete o dado, que é idêntico para as 1.200 lojas |
| Corrida entre leitura em voo e invalidação | TTL como teto do pior caso | a ordem certa reduz a chance da corrida; o TTL limita o dano quando ela acontece mesmo assim |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Debandada sob TTL sincronizado | jitter por chave | concorrência dentro da janela de UM miss — é a trava |
| Reconstrução concorrente da mesma chave | trava `SET NX` | debandada entre chaves DIFERENTES expirando juntas — é o jitter |
| Preço desatualizado além do tolerado | `DEL` disparado pela escrita | a corrida rara entre leitura em voo e o próprio `DEL` |
| Corrida entre leitura em voo e invalidação | TTL como teto | valer no segundo seguinte — para isso, confirmar ausência da chave |
| Queda de um nó de cache | Multi-AZ com failover automático | perda total de região — fora do escopo deste laboratório |
| Acesso não autorizado ao cache | AUTH + TLS + security group restrito | processo comprometido DENTRO da task, que já tem a credencial |
- O POS pede o preço de um SKU; a aplicação consulta o cache primeiro.
- HIT: devolve em poucos milissegundos, sem tocar o banco.
- MISS: a aplicação disputa a trava de reconstrução daquela chave específica.
- Quem ganha consulta a réplica do L07 e grava o resultado com TTL base + jitter.
- Quem perde espera um instante e tenta o cache de novo, ou segue sem popular.
- Uma alteração de preço grava no primário e aguarda o commit ser confirmado.
- Só então a aplicação envia `DEL` para a chave — nunca `SET` com o valor novo.
- A próxima leitura daquele SKU é um miss garantido, e repovoa do dado já commitado.
- Se um nó de cache cai, o failover automático promove a réplica sem trocar o endpoint.
- O painel mede taxa de acerto, não só latência — é o sinal que prova que o cache funciona.
Perguntas frequentes
❓ Cache-aside ou write-through: qual usar para um catálogo de produtos?
❓ O que é debandada de cache, e por que ela só aparece sob carga real?
❓ TTL e invalidação explícita resolvem o mesmo problema?
❓ Por que a chave de cache não deve incluir o identificador da loja?
❓ O ElastiCache Multi-AZ substitui a réplica de leitura do RDS que já existe?
❓ Preciso de trava de reconstrução mesmo com poucos usuários simultâneos?
❓ Por que usar DEL em vez de atualizar o cache direto na escrita?
❓ O endpoint primário do ElastiCache muda depois de um failover?
Fixando
A trava de reconstrução foi implementada com `SET NX`, sem prazo de expiração. Um processo trava no meio da reconstrução e nunca libera a chave de trava. Qual é a consequência mais provável?
A aplicação grava o novo preço no RDS, aguarda o commit, e só então envia `DEL` para a chave no Redis. Mesmo assim, um cliente às vezes recebe o preço antigo por até 30 segundos depois da alteração. Qual é a explicação mais provável?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L07 no ar (réplica de leitura, Multi-AZ do RDS, pool dimensionado), noção básica de Redis |
| Conhecimentos adquiridos | os três padrões de cache e o modo de falha de cada um; debandada de cache e as duas defesas (jitter e trava); a diferença entre TTL e invalidação explícita; o bug de leitura concorrente que repopula com valor velho, e por que o TTL o limita sem eliminá-lo; desenho de chave como decisão de taxa de acerto |
| Limitação que fica | a corrida entre leitura em voo e invalidação continua possível, limitada ao TTL — para a promoção que precisa valer no segundo seguinte, a rotina do operador confirma a ausência da chave antes de anunciar sucesso, o que este laboratório não automatiza |
| Próximo exemplo recomendado | L89 — custo e latência de GenAI, que aplica a mesma decisão de TTL (quanto tempo você tolera estar errado) a cache de prompt de modelo, um domínio onde "errado" custa token em vez de consulta ao banco |
| Também habilitado por este módulo | L17 (upload direto ao S3) aparece na cadeia de evolução do catálogo, ainda que não reaproveite o cache diretamente; o padrão de decidir "quanto tempo tolerar estar errado" volta a aparecer lá para URL pré-assinada |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Minimizing downtime in ElastiCache by using Multi-AZ with Valkey and Redis OSS — o mecanismo de failover automático e o comportamento do endpoint primário; Finding connection endpoints in ElastiCache — a distinção entre endpoint primário e endpoint de leitura; Managing reserved memory for Valkey and Redis OSS — a política de evicção padrão `volatile-lru`; e o whitepaper Database Caching Strategies Using Redis, seção de validade de cache, que descreve jitter de TTL como defesa contra debandada. Os valores de preço não aparecem neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado, e você deve conferir na sua conta
Os números de volume — 1.200 lojas, 500 SKUs quentes, p95 de 340 ms, 80% de CPU na réplica — são os medidos na aplicação de exemplo da Cadência, e servem como ordem de grandeza, não como referência. O TTL base de 30 s e o jitter de até 10 s valem para a tolerância de negócio DESTE catálogo; meça a sua própria tolerância antes de copiar o número. Da mesma forma, o throughput de comandos por segundo que satura um nó `cache.r7g.large` não foi medido neste laboratório — confirme na sua carga real antes de dimensionar por ele.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…