Redis como cache: estratégias e invalidação
1. Fundamentos do Cache com Redis
Bancos de dados SQL relacionais são excelentes para garantir consistência e integridade dos dados, mas enfrentam limitações de desempenho sob alta carga de leitura. O Redis surge como uma camada de cache distribuído que reduz a latência e alivia a pressão sobre o banco principal.
Diferença entre cache local e distribuído:
- Cache local (in-memory): armazenado na memória do próprio servidor da aplicação. Rápido, mas não compartilhado entre instâncias.
- Cache distribuído (Redis): centralizado e acessível por múltiplos servidores. Ideal para arquiteturas escaláveis.
Casos de uso típicos:
- Consultas SQL repetitivas (relatórios, dashboards)
- Sessões de usuário
- Resultados de agregações complexas (COUNT, SUM, GROUP BY)
- Listas de produtos, categorias e configurações
2. Estratégias de População do Cache
Cache-aside (Lazy Loading)
A aplicação verifica primeiro o cache. Se não encontrar, busca no banco SQL e armazena no Redis.
function getUserById(id):
user = redis.get("user:" + id)
if user is None:
user = sql.query("SELECT * FROM users WHERE id = ?", id)
redis.set("user:" + id, user, ttl=3600)
return user
Write-through
Toda gravação no banco SQL é replicada imediatamente para o Redis.
function updateUser(id, data):
sql.execute("UPDATE users SET name = ? WHERE id = ?", data.name, id)
redis.set("user:" + id, data, ttl=3600)
Write-behind (Write-back)
As atualizações são acumuladas em filas (Redis Streams) e processadas em lote no banco SQL.
function batchUpdateUsers(updates):
for update in updates:
redis.xadd("user_updates", update)
# Processo assíncrono lê a fila e persiste no SQL
3. Políticas de Expiração de Chaves no Redis
TTL fixo e expiração por janela de tempo
O Redis permite expirar chaves automaticamente após um tempo definido.
# TTL fixo de 10 minutos
redis.set("cache:report:2024", data, ex=600)
# Expiração em timestamp específico
redis.expireat("cache:report:2024", 1700000000)
Comandos de expiração
# Define expiração em segundos
EXPIRE chave 3600
# Define expiração em milissegundos
PEXPIRE chave 3600000
# Expiração em lote com SCAN
cursor = 0
loop:
cursor, keys = redis.scan(cursor, match="cache:temp:*")
for key in keys:
redis.expire(key, 300)
if cursor == 0:
break
Estratégias de expiração
- Lazy: Remove chaves expiradas apenas quando acessadas
- Active: O Redis verifica periodicamente chaves expiradas
- Volatile: Remove apenas chaves com TTL definido
4. Estratégias de Invalidação de Cache
Invalidação manual
Ao alterar dados no banco SQL, removemos explicitamente a chave no Redis.
function deleteUser(id):
sql.execute("DELETE FROM users WHERE id = ?", id)
redis.delete("user:" + id)
Invalidação por padrão de chave
Útil para invalidar múltiplas chaves relacionadas.
# Usando SCAN para evitar bloqueio
cursor = 0
loop:
cursor, keys = redis.scan(cursor, match="user:*")
for key in keys:
redis.delete(key)
if cursor == 0:
break
Versionamento de chaves
Cada versão dos dados recebe um sufixo numérico, facilitando a invalidação.
# Ao atualizar dados
version = redis.incr("user:version:123")
redis.set("user:123:v" + version, data, ttl=3600)
# Consulta sempre busca a versão mais recente
current_version = redis.get("user:version:123")
data = redis.get("user:123:v" + current_version)
5. Consistência entre Cache e Banco de Dados
Problemas de consistência
- Stale data: dados desatualizados no cache após alteração no SQL
- Cache stampede: múltiplas requisições simultâneas recalculam o cache
Técnicas para evitar dados obsoletos
- Locking distribuído: apenas uma requisição recalcula o cache
- Dupla gravação: atualizar cache e banco na mesma transação
- TTL curto: aceitar inconsistência temporária em troca de performance
Uso de filas para invalidação em cascata
# Gatilho no banco SQL envia evento para Redis Stream
sql.execute("INSERT INTO audit_log (action, entity_id) VALUES ('update', ?)", id)
redis.xadd("invalidation_queue", {"entity": "user", "id": id})
# Worker processa a fila e invalida caches relacionados
stream = redis.xread({"invalidation_queue": "0"}, block=0)
for event in stream:
entity = event["entity"]
id = event["id"]
redis.delete(f"{entity}:{id}")
redis.delete(f"{entity}:{id}:details")
6. Monitoramento e Otimização do Cache
Métricas essenciais
# Taxa de acerto (hit rate)
INFO stats
# keyspace_hits / (keyspace_hits + keyspace_misses)
# Memória utilizada
INFO memory
# used_memory_human
# Comandos lentos
SLOWLOG GET 10
Diagnóstico com comandos nativos
# Verificar memória de uma chave específica
MEMORY USAGE user:123
# Tamanho total do cache
DBSIZE
# Padrões de acesso
SCAN 0 MATCH user:* COUNT 1000
Ajuste dinâmico de TTL
Baseado em frequência de acesso, podemos aumentar ou diminuir o TTL.
# Cache frio: TTL curto
if access_count < 10:
redis.expire(key, 60)
# Cache quente: TTL longo
if access_count > 100:
redis.expire(key, 3600)
7. Casos Práticos com SQL e Redis
Cache de consultas SQL complexas
# Consulta pesada com JOINs
SELECT p.id, p.name, c.name as category, AVG(r.rating) as avg_rating
FROM products p
JOIN categories c ON p.category_id = c.id
LEFT JOIN reviews r ON p.id = r.product_id
GROUP BY p.id;
# Cache do resultado
cache_key = "product:report:top_sellers"
result = redis.get(cache_key)
if result is None:
result = sql.execute(query)
redis.set(cache_key, result, ex=300) # Expira em 5 minutos
Cache de sessão de usuário com expiração por inatividade
# Ao fazer login
session_id = generate_uuid()
redis.set("session:" + session_id, {"user_id": 123, "role": "admin"}, ex=1800)
# A cada requisição, renova o TTL
redis.expire("session:" + session_id, 1800)
# Ao fazer logout
redis.delete("session:" + session_id)
Pipeline de invalidação com gatilhos no banco
# Trigger no PostgreSQL
CREATE OR REPLACE FUNCTION invalidate_cache()
RETURNS trigger AS $$
BEGIN
PERFORM pg_notify('cache_invalidation',
json_build_object('table', TG_TABLE_NAME,
'id', NEW.id)::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER user_update_trigger
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION invalidate_cache();
# Aplicação escuta o canal e invalida o Redis
import psycopg2
conn = psycopg2.connect("dbname=test")
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cursor = conn.cursor()
cursor.execute("LISTEN cache_invalidation;")
while True:
conn.poll()
while conn.notifies:
notify = conn.notifies.pop()
data = json.loads(notify.payload)
redis.delete(f"{data['table']}:{data['id']}")
Conclusão
O Redis como cache ao lado de bancos SQL oferece ganhos significativos de performance quando implementado com estratégias adequadas de população, expiração e invalidação. A escolha entre cache-aside, write-through ou write-behind depende do nível de consistência necessário. Monitoramento constante de hit rate e uso de memória permite ajustes finos no TTL e nas políticas de expiração, garantindo que o cache permaneça eficiente sem comprometer a integridade dos dados.
Referências
- Documentação Oficial do Redis - Explicação sobre expiração de chaves — Guia completo sobre TTL, expiração lazy e active no Redis
- Redis como Cache - Padrões e Estratégias — Documentação oficial abordando cache-aside, write-through e write-behind
- Estratégias de Invalidação de Cache — Artigo técnico em português sobre padrões de invalidação com exemplos práticos
- Redis Streams para Invalidação em Cascata — Tutorial oficial sobre filas e streams para sincronização de dados
- Monitoramento de Cache com Redis INFO — Referência completa dos comandos de diagnóstico e métricas de desempenho
- PostgreSQL LISTEN/NOTIFY com Redis — Documentação oficial do PostgreSQL sobre notificações assíncronas para integração com cache
- Cache Stampede e Locking Distribuído — Artigo de Martin Kleppmann sobre prevenção de cache stampede com locks distribuídos