kb.erickguedes.com
Redis: Cache, Fila e Tempo Real

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.