Cache
Estratégias para guardar respostas caras, e o problema difícil de saber quando descartá-las.
Os exemplos guardam no Redis os dados de um produto, com o cliente de Redis mais usado em cada linguagem. A função buscarProdutoNoBanco representa a consulta ao banco.
Cache-aside com Redis
O roteiro é o mesmo nas cinco linguagens: procurar no cache; se não achar, buscar no banco e guardar com um TTL que tem uma pequena variação aleatória.
import { createClient } from "redis";
const redis = await createClient({ url: process.env.REDIS_URL }).connect();
const TTL_SEGUNDOS = 300;
export async function buscarProduto(id: number) {
const chave = `produto:${id}`;
const emCache = await redis.get(chave);
if (emCache) {
return JSON.parse(emCache) as Produto;
}
const produto = await buscarProdutoNoBanco(id);
if (produto) {
const ttl = TTL_SEGUNDOS + Math.floor(Math.random() * 30);
await redis.set(chave, JSON.stringify(produto), { EX: ttl });
}
return produto;
}import json
import os
import random
import redis
cliente = redis.Redis.from_url(os.environ["REDIS_URL"])
TTL_SEGUNDOS = 300
def buscar_produto(produto_id: int) -> dict | None:
chave = f"produto:{produto_id}"
em_cache = cliente.get(chave)
if em_cache is not None:
return json.loads(em_cache)
produto = buscar_produto_no_banco(produto_id)
if produto is not None:
ttl = TTL_SEGUNDOS + random.randint(0, 30)
cliente.set(chave, json.dumps(produto), ex=ttl)
return produtopackage catalogo
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand/v2"
"time"
"github.com/redis/go-redis/v9"
)
const ttlBase = 5 * time.Minute
type Catalogo struct {
rdb *redis.Client
}
func (c *Catalogo) BuscarProduto(ctx context.Context, id int64) (*Produto, error) {
chave := fmt.Sprintf("produto:%d", id)
emCache, err := c.rdb.Get(ctx, chave).Bytes()
if err == nil {
var p Produto
if err := json.Unmarshal(emCache, &p); err == nil {
return &p, nil
}
} else if !errors.Is(err, redis.Nil) {
// Redis fora do ar: segue para o banco em vez de falhar
}
p, err := buscarProdutoNoBanco(ctx, id)
if err != nil || p == nil {
return p, err
}
if dados, err := json.Marshal(p); err == nil {
ttl := ttlBase + time.Duration(rand.IntN(30))*time.Second
c.rdb.Set(ctx, chave, dados, ttl)
}
return p, nil
}import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.concurrent.ThreadLocalRandom;
import redis.clients.jedis.JedisPooled;
public class Catalogo {
private static final long TTL_SEGUNDOS = 300;
private final JedisPooled redis = new JedisPooled("localhost", 6379);
private final ObjectMapper json = new ObjectMapper();
public Produto buscarProduto(long id) throws Exception {
String chave = "produto:" + id;
String emCache = redis.get(chave);
if (emCache != null) {
return json.readValue(emCache, Produto.class);
}
Produto produto = buscarProdutoNoBanco(id);
if (produto != null) {
long ttl = TTL_SEGUNDOS + ThreadLocalRandom.current().nextLong(30);
redis.setex(chave, ttl, json.writeValueAsString(produto));
}
return produto;
}
}using System.Text.Json;
using StackExchange.Redis;
public class Catalogo(IConnectionMultiplexer conexao)
{
private static readonly TimeSpan TtlBase = TimeSpan.FromMinutes(5);
private readonly IDatabase redis = conexao.GetDatabase();
public async Task<Produto?> BuscarProduto(long id)
{
var chave = $"produto:{id}";
var emCache = await redis.StringGetAsync(chave);
if (emCache.HasValue)
{
return JsonSerializer.Deserialize<Produto>(emCache.ToString());
}
var produto = await BuscarProdutoNoBanco(id);
if (produto is not null)
{
var ttl = TtlBase + TimeSpan.FromSeconds(Random.Shared.Next(30));
await redis.StringSetAsync(chave, JsonSerializer.Serialize(produto), ttl);
}
return produto;
}
}O que muda de uma linguagem para outra
| Cliente de Redis | Como sabe que a chave não existe | |
|---|---|---|
| Node | redis (node-redis) | get devolve null |
| Python | redis-py | get devolve None |
| Go | go-redis v9 | O erro é redis.Nil, que precisa ser diferenciado de uma falha real |
| Java | Jedis | get devolve null |
| C# | StackExchange.Redis | RedisValue.HasValue é falso |
O ponto que mais varia é o tratamento de falhas do Redis. Se o Redis cair, o exemplo em Go segue para o banco. Nos outros, uma exceção de conexão sobe e a requisição falha, mesmo com o banco funcionando. Em produção, envolva a leitura e a gravação no cache num tratamento de erro que registre a falha e continue pelo banco: o cache deve deixar o sistema mais rápido, e não mais frágil.
Para cache em memória do processo, cada linguagem tem uma biblioteca de referência: lru-cache no Node, cachetools ou functools.lru_cache em Python, ristretto em Go, Caffeine em Java e IMemoryCache ou HybridCache no .NET. O HybridCache, do .NET 9, combina memória e cache distribuído e já protege contra a manada.
Invalidar depois de alterar
Quando o produto muda, a cópia em cache é apagada depois que a alteração foi confirmada no banco:
export async function alterarPreco(id: number, novoPreco: number) {
await db.transaction(async (tx) => {
await tx.query("UPDATE produtos SET preco = $1 WHERE id = $2", [novoPreco, id]);
});
// Depois do COMMIT: a próxima leitura busca o valor novo no banco
await redis.del(`produto:${id}`);
}| Ordem | O que pode dar errado |
|---|---|
Apagar o cache antes do COMMIT | Uma leitura no meio do caminho busca o valor antigo no banco e o grava de volta no cache |
Apagar o cache depois do COMMIT | Se o del falhar, o valor antigo fica até o TTL vencer. Por isso o TTL existe |
| Gravar o novo valor no cache em vez de apagar | Duas alterações simultâneas podem deixar no cache o valor mais antigo |
Quando há muitos caminhos que alteram o mesmo dado, centralize a invalidação numa função única, ou publique um evento de alteração que um único consumidor transforma em invalidações.
Proteger as chaves mais lidas da manada
Para chaves que concentram muito tráfego, só uma requisição deve buscar na origem quando o cache estiver vazio. Uma forma simples, entre várias instâncias, é uma trava curta no próprio Redis com SET ... NX:
import { randomUUID } from "node:crypto";
export async function buscarComTrava(id: number) {
const chave = `produto:${id}`;
const chaveTrava = `trava:${chave}`;
for (let tentativa = 0; tentativa < 50; tentativa++) {
const emCache = await redis.get(chave);
if (emCache) return JSON.parse(emCache) as Produto;
// Só uma requisição consegue a trava; ela expira sozinha em 5 segundos
const dono = randomUUID();
const obteve = await redis.set(chaveTrava, dono, { NX: true, PX: 5000 });
if (obteve) {
try {
const produto = await buscarProdutoNoBanco(id);
if (produto) {
await redis.set(chave, JSON.stringify(produto), {
EX: 300 + Math.floor(Math.random() * 30),
});
}
return produto;
} finally {
// Só apaga a trava se ainda for a dona dela
if ((await redis.get(chaveTrava)) === dono) await redis.del(chaveTrava);
}
}
// Outra requisição está buscando: espera um pouco e confere o cache de novo
await new Promise((r) => setTimeout(r, 50));
}
return buscarProdutoNoBanco(id); // não esperar para sempre
}Dentro de um único processo, a mesma ideia é mais simples: guardar a promessa ou a tarefa em andamento num mapa e fazer as outras requisições esperarem por ela. Go tem isso pronto no pacote golang.org/x/sync/singleflight.
Configurar o Redis como cache
# redis.conf, ou parâmetros do serviço gerenciado
maxmemory 2gb
maxmemory-policy allkeys-lfuE acompanhe três números:
| Métrica | Onde ver | Sinal de alerta |
|---|---|---|
| Taxa de acerto | keyspace_hits e keyspace_misses em INFO stats | Abaixo do esperado: TTL curto demais, chaves mal desenhadas ou dado que não deveria estar em cache |
| Itens descartados | evicted_keys em INFO stats | Crescendo rápido: memória pequena para o volume |
| Memória usada | used_memory em INFO memory | Perto de maxmemory o tempo todo |
Lista de revisão
Leitura
- Todo item gravado tem TTL, com variação aleatória?
- Uma falha do Redis faz o código seguir pelo banco, em vez de falhar a requisição?
- A chave inclui tudo o que muda a resposta, inclusive o usuário?
- As chaves mais lidas têm proteção contra a manada?
Escrita
- Todo caminho que altera o dado invalida o cache?
- A invalidação acontece depois do
COMMIT? - O código apaga a chave, em vez de gravar o novo valor?
Operação
- O Redis tem
maxmemorye uma política de descarte configurados? - Dados que não podem ser descartados estão fora do Redis de cache?
- A taxa de acerto e os itens descartados são monitorados?
- Nenhuma decisão de transação, como saldo ou estoque na compra, depende de um valor em cache?
Prompts para aprofundar
Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.
Implementar cache-aside passo a passo
Quero implementar cache-aside com Redis do jeito certo, na minha linguagem. Pergunte qual linguagem, framework e cliente de Redis eu uso. Vamos em etapas. Em cada uma, deixe que eu tente e depois mostre a sua versão, comentando as diferenças: 1. Subir um Redis com Docker e conectar a aplicação. 2. A leitura com cache-aside, com TTL e variação aleatória. 3. O tratamento de falhas do Redis, para que o sistema continue funcionando pelo banco. 4. A invalidação depois da alteração, na ordem certa em relação ao COMMIT. 5. A proteção contra a manada para a chave mais lida. 6. Testes automatizados para acerto, falha, invalidação e Redis fora do ar. No final, me ajude a medir a taxa de acerto e o ganho de latência com uma carga simulada.
Cache em duas camadas
Quero montar um cache em duas camadas: memória do processo na frente de um Redis. Pergunte qual linguagem eu uso e quantas instâncias da aplicação existem. Explique, com código: 1. Qual biblioteca de cache em memória usar na minha linguagem e como configurá-la com tamanho máximo e TTL. 2. A leitura que confere a memória, depois o Redis, depois o banco, e grava nas camadas que faltaram. 3. Como escolher os TTLs de cada camada. 4. Como invalidar a camada em memória de todas as instâncias quando o dado muda, usando publicação e assinatura no Redis. 5. O que fazer quando a mensagem de invalidação se perde. Se a minha plataforma já oferece algo pronto para isso, como o HybridCache no .NET, mostre essa opção também.
Revisar o uso de cache
Vou colar abaixo código que usa cache. Revise com foco em correção e segurança, e não só em desempenho. Verifique, nesta ordem, e para cada item diga se está correto, incorreto ou ausente, citando a linha: 1. Todo item gravado tem TTL? Há variação aleatória? 2. A chave inclui tudo o que muda a resposta, inclusive o usuário quando o dado é pessoal? 3. Todo caminho que altera o dado invalida o cache, depois do COMMIT? 4. O código apaga a chave ou grava o novo valor? Há risco de condição de corrida? 5. Uma falha do cache derruba a requisição ou o código segue pela origem? 6. As chaves mais lidas têm proteção contra a manada? 7. Algum dado usado para decidir uma transação vem do cache? 8. Algum dado só existe no cache? Para cada problema, descreva o cenário concreto em que ele causa um erro ou um vazamento, e mostre a correção. Não reescreva o que já está correto. Código: [cole aqui]