Cache: Padrões e Estratégias
Aula 2 de 6
Cache Patterns
Cache Aside (Lazy Loading)
# Application lê do cache primeiro, se não existe busca no DB
# Padrão mais comum e flexível
# Pseudocódigo (Node.js)
# async function getUser(id) {
# // 1. Tentar cache
# let user = await redis.get(`user:${id}`);
# if (user) return JSON.parse(user);
#
# // 2. Cache miss: buscar no DB
# user = await db.query('SELECT * FROM users WHERE id = $1', [id]);
#
# // 3. Popular cache com TTL
# await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
# return user;
# }
redis-cli << 'REDIS'
-- Simular Cache Aside
SET user:1001 '{"id":1001,"nome":"Maria","email":"[email protected]"}' EX 3600
GET user:1001
REDIS
Read Through
# Cache é responsável por buscar do DB quando há cache miss
# Redis não faz isso nativamente — implementado via application code
# Ou usando módulos como Redis-OM com Spring Cache
# Spring Boot @Cacheable
# @Cacheable(value = "users", key = "#id")
# public User getUser(Long id) {
# return userRepository.findById(id).orElse(null);
# }
Write Through
# Toda escrita vai primeiro para o cache, depois para o DB
# Garante que cache está sempre consistente
# async function updateUser(id, data) {
# // 1. Atualizar cache primeiro
# await redis.set(`user:${id}`, JSON.stringify(data));
#
# // 2. Depois atualizar DB
# await db.query('UPDATE users SET ... WHERE id = $1', [id, data]);
# }
Write Behind (Write Back)
# Escrita vai apenas para o cache, que persiste no DB assincronamente
# Alta performance, risco de perda se o Redis cair
# async function writeBehind(id, data) {
# // 1. Escrever no cache
# await redis.set(`user:${id}`, JSON.stringify(data));
#
# // 2. Enfileirar para persistência
# await redis.lpush('persist:queue', JSON.stringify({id, data}));
# }
#
# Background worker:
# while (task = await redis.brpop('persist:queue', 0)) {
# await db.query('UPDATE users SET ... WHERE id = $1', [task.id, task.data]);
# }
Refresh Ahead
# Renovar cache antes de expirar (preventivo)
# Útil para dados frequentemente acessados
# async function getWithRefresh(key, ttl, fetchFn) {
# // Verificar se está perto de expirar
# const remaining = await redis.ttl(key);
#
# if (remaining < 60) {
# // Refresh em background (não bloqueante)
# const data = await fetchFn();
# await redis.set(key, JSON.stringify(data), 'EX', ttl);
# }
#
# const cached = await redis.get(key);
# return JSON.parse(cached);
# }
TTL Strategy
# Estratégias de TTL baseadas no tipo de dado
# Dados estáveis (cidades, categorias): TTL longo
SET categorias:lista '[...]' EX 86400 # 24h
# Dados semi-estáveis (perfil de usuário): TTL médio
SET user:1001 '...' EX 3600 # 1h
# Dados voláteis (preços, estoque): TTL curto
SET estoque:1001 50 EX 300 # 5 min
# Sessões: TTL com sliding expiration
SET session:abc123 '...' EX 1800 # 30 min
# Renovar a cada acesso
EXPIRE session:abc123 1800 # reset TTL
# Dados agregados: TTL baseado em janela
SET relatorio:vendas:hoje '...' EX 60 # 1 min
Eviction Policies
# Configurar política no redis.conf
maxmemory 512mb
maxmemory-policy allkeys-lru
# Políticas de evicção:
# noeviction: retorna erro quando memória cheia
# allkeys-lru: remove as chaves menos recentemente usadas
# allkeys-lfu: remove as chaves menos frequentemente usadas
# volatile-lru: remove LRU apenas de chaves com TTL
# volatile-lfu: remove LFU apenas de chaves com TTL
# volatile-ttl: remove chaves com TTL mais curto primeiro
# allkeys-random: remove chaves aleatórias
# volatile-random: remove aleatório de chaves com TTL
# Testar política atual
redis-cli CONFIG GET maxmemory-policy
redis-cli CONFIG GET maxmemory
# Alterar em runtime
redis-cli CONFIG SET maxmemory-policy allkeys-lfu
redis-cli CONFIG SET maxmemory 1gb
LRU vs LFU
# LRU (Least Recently Used): foca em recência de acesso
# Bom para: caches gerais, sessões, tokens
# maxmemory-policy allkeys-lru
# LFU (Least Frequently Used): foca em frequência de acesso
# Bom para: conteúdo popular, rankings, dados frequentemente acessados
# maxmemory-policy allkeys-lfu
# No Redis, LFU tem dois sub-parâmetros:
# lfu-log-factor: ajusta contador de frequência (default 10)
# lfu-decay-time: tempo para decrementar contador (default 1 minuto)
CONFIG SET lfu-log-factor 10
CONFIG SET lfu-decay-time 1
Cache Stampede (Dogpile Effect)
# Problema: quando cache expira e múltiplas requisições simultâneas
# batem no DB ao mesmo tempo (thundering herd)
# Solução 1: Mutex Lock
# async function getWithMutex(key, ttl, fetchFn) {
# const cached = await redis.get(key);
# if (cached) return JSON.parse(cached);
#
# // Tentar lock (apenas uma requisição popula)
# const lock = await redis.set(`lock:${key}`, '1', 'NX', 'EX', 10);
# if (lock) {
# const data = await fetchFn();
# await redis.set(key, JSON.stringify(data), 'EX', ttl);
# return data;
# }
#
# // Outras requisições esperam um pouco e tentam ler
# await sleep(100);
# return getWithMutex(key, ttl, fetchFn);
# }
# Solução 2: TTL com desfoque (staggered TTL)
# SET popular:key '...' EX 3600 # TTL real
# SET popular:key:stale '...' EX 5400 # TTL estendido (50% a mais)
# A aplicação tenta primeiro a chave real, se expirou usa a stale
# e agenda refresh em background
# Solução 3: Refresh Ahead (preventivo)
# Se TTL < threshold, atualizar em background
Redis como Cache LRU
# Configuração de cache LRU dedicado
redis-cli << 'REDIS'
CONFIG SET maxmemory 256mb
CONFIG SET maxmemory-policy allkeys-lru
CONFIG SET maxmemory-samples 10 # amostras para LRU (default 5)
REDIS
# maxmemory-samples: quanto maior, mais preciso o LRU
# mas custa mais CPU
# 5 = eficiente (padrão)
# 10 = boa precisão
Spring Cache / Node Cache
ioredis (Node.js)
const Redis = require('ioredis');
const redis = new Redis({ host: 'localhost', port: 6379 });
async function getOrFetch(key, ttl, fetchFn) {
const cached = await redis.get(key);
if (cached) {
console.log('Cache HIT:', key);
return JSON.parse(cached);
}
console.log('Cache MISS:', key);
const data = await fetchFn();
await redis.set(key, JSON.stringify(data), 'EX', ttl);
return data;
}
// Uso
const user = await getOrFetch('user:1001', 3600, async () => {
return { id: 1001, nome: 'Maria' };
});
node-cache-manager
const { createCache } = require('cache-manager');
const { redisStore } = require('cache-manager-ioredis-yet');
const cache = await createCache({
stores: [
// Cache em memória (L1)
{
store: 'memory',
max: 100,
ttl: 60 * 1000, // 1 minuto
},
// Redis (L2)
await redisStore({
redis: { host: 'localhost', port: 6379 },
ttl: 3600 * 1000, // 1 hora
}),
],
});
// Multi-tier caching (L1 memoria, L2 Redis)
const user = await cache.get('user:1001');
if (!user) {
const user = await db.findUser(1001);
await cache.set('user:1001', user);
}
Lab: Cache Estratégico com Refrescância
cat > cache-strategies.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
echo "=== LAB DE ESTRATÉGIAS DE CACHE ==="
# 1. Cache Aside com Lock
echo ""
echo "--- 1. Cache Aside com Mutex ---"
simular_cache_aside() {
local key="product:${1}"
local ttl="${2:-60}"
local lock_key="lock:${key}"
# Cache HIT?
local cached
cached=$(redis-cli GET "$key")
if [[ -n "$cached" ]]; then
echo "Cache HIT: $key = $cached (TTL: $(redis-cli TTL $key)s)"
return
fi
echo "Cache MISS: $key"
# Tentar lock (NX)
local locked
locked=$(redis-cli SET "$lock_key" "1" NX EX 10)
if [[ "$locked" == "OK" ]]; then
echo "Lock adquirido. Buscando do DB..."
# Simular DB query
local data='{"id":'$1',"nome":"Produto '$1'","preco":99.90}'
redis-cli SET "$key" "$data" EX "$ttl"
redis-cli DEL "$lock_key"
echo "Cache populado: $data"
else
echo "Lock não adquirido. Aguardando..."
sleep 0.5
simular_cache_aside "$1" "$ttl"
fi
}
# Simular 3 chamadas concorrentes
simular_cache_aside 42 60
simular_cache_aside 42 60 &
simular_cache_aside 42 60 &
wait
# 2. Eviction Policy Demo
echo ""
echo "--- 2. Teste de Eviction ---"
redis-cli CONFIG SET maxmemory 1mb 2>/dev/null || true
redis-cli CONFIG SET maxmemory-policy allkeys-lru 2>/dev/null || true
echo "Populando chaves até estourar a memória..."
for i in $(seq 1 5000); do
redis-cli SET "eviction:test:$i" "$(python3 -c "print('x'*1024)" 2>/dev/null || yes x | head -1024 | tr -d '\n')" EX 3600 2>/dev/null || true
done
echo "Chaves atuais: $(redis-cli DBSIZE)"
echo ""
echo "--- 3. Cache com Refresh Ahead ---"
cat > refresh-daemon.sh << 'DAEMON'
#!/bin/bash
# Daemon que mantém cache quente para chaves populares
set -euo pipefail
REFRESH_THRESHOLD=60 # segundos
TTL=300 # TTL completo
while true; do
# Buscar chaves mais acessadas via LFU
redis-cli --scan --pattern 'hot:*' | while read -r key; do
remaining=$(redis-cli TTL "$key")
if [[ "$remaining" -lt "$REFRESH_THRESHOLD" ]]; then
echo "Refrescando: $key (TTL=$remaining)"
# Simular refresh (buscar dados e repopular)
redis-cli SET "$key" "$(date)" EX "$TTL"
fi
done
sleep 30
done
DAEMON
chmod +x refresh-daemon.sh
echo "refresh-daemon.sh criado (execute em background)"
# 4. Cleanup
echo ""
echo "--- 4. Limpeza ---"
redis-cli EVAL "return redis.call('DEL', unpack(redis.call('KEYS', 'product:*')))" 0 2>/dev/null || true
redis-cli EVAL "return redis.call('DEL', unpack(redis.call('KEYS', 'lock:*')))" 0 2>/dev/null || true
echo "Dados de teste removidos."
SCRIPT
chmod +x cache-strategies.sh
Cache Aside é o padrão mais comum. allkeys-lru ou allkeys-lfu são políticas de evicção seguras. Mutex lock previne cache stampede. Multi-tier cache (L1 memory + L2 Redis) otimiza latência. TTL com refresh ahead mantém dados quentes.