Estratégias de cache em múltiplas camadas

1. Fundamentos do Cache Multicamadas

1.1. Definição e objetivos do cache em múltiplas camadas

Cache em múltiplas camadas é uma arquitetura que armazena dados temporários em diferentes níveis hierárquicos, desde o cliente até o banco de dados. O objetivo principal é reduzir a latência de acesso a dados, diminuir a carga em servidores de origem e melhorar a experiência do usuário. Cada camada opera com diferentes capacidades de armazenamento, velocidades de acesso e políticas de expiração.

1.2. Latência e gargalos: do cliente ao banco de dados

A latência em sistemas web varia drasticamente entre camadas:
- Cache L1 (navegador): 0-5ms
- Cache L2 (CDN): 10-50ms
- Cache L3 (aplicação): 1-5ms (em memória)
- Cache L4 (banco): 5-20ms (buffer pool)
- Banco de dados sem cache: 50-500ms

Gargalos típicos incluem consultas repetitivas ao banco, assets não cacheados e TTLs mal configurados.

1.3. Hierarquia de cache: L1, L2, L3, L4

A hierarquia segue o princípio de localidade temporal e espacial:
- L1 – Navegador: cache local do usuário (Cache Storage, HTTP cache)
- L2 – CDN: cache em servidores edge geograficamente distribuídos
- L3 – Aplicação: cache em memória (Redis, Memcached) no servidor de aplicação
- L4 – Banco: cache interno do SGBD (buffer pool, query cache)

2. Cache na Camada do Cliente (L1)

2.1. Cache de navegador: cabeçalhos HTTP

Cache-Control: public, max-age=3600, immutable
ETag: "abc123"
Expires: Wed, 21 Oct 2025 07:28:00 GMT

Exemplo de resposta HTTP com cache otimizado:

HTTP/1.1 200 OK
Content-Type: text/css
Cache-Control: public, max-age=31536000, immutable
ETag: "v2.1.3-style-min"
Last-Modified: Tue, 15 Mar 2024 10:30:00 GMT

2.2. Service Workers e Cache API para aplicações PWA

Registro de Service Worker com cache de assets:

// service-worker.js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('app-v2').then((cache) => {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles/main.css',
        '/scripts/app.js',
        '/images/logo.png'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

2.3. Estratégias de invalidação: stale-while-revalidate e versionamento

# Estratégia stale-while-revalidate para assets dinâmicos
Cache-Control: public, max-age=600, stale-while-revalidate=86400

# Versionamento de assets via hash no nome do arquivo
<link rel="stylesheet" href="/styles/main.a1b2c3d4.css">
<script src="/scripts/app.e5f6g7h8.js"></script>

3. Cache na Camada de CDN (L2)

3.1. Funcionamento de CDNs: edge caching e distribuição geográfica

CDNs armazenam conteúdo em servidores edge próximos aos usuários. Exemplo de configuração com Cloudflare:

# Regras de cache para conteúdo estático
# Arquivos com extensão .css, .js, .png, .jpg
# Cache TTL: 30 dias (2592000 segundos)
# Edge Cache TTL: 30 dias
# Browser Cache TTL: 7 dias

# Regra para conteúdo dinâmico com cache controlado
# URL: /api/*
# Edge Cache TTL: 60 segundos
# Bypass on cookie: session_id

3.2. Purga e invalidação seletiva de cache em CDN

# Purga seletiva via API (exemplo Cloudflare)
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
  -H "Authorization: Bearer API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://exemplo.com/produtos/novo-lancamento.html"]}'

# Purga por tag (se suportado)
--data '{"tags":["produto:12345","categoria:10"]}'

3.3. Cache de conteúdo dinâmico vs. estático: TTLs e regras

# Conteúdo estático: TTL longo (30 dias)
Cache-Control: public, max-age=2592000, immutable

# Conteúdo dinâmico com variação por usuário: sem cache CDN
Cache-Control: private, no-cache, no-store

# Conteúdo dinâmico semi-estático: TTL curto (5 minutos)
Cache-Control: public, max-age=300, s-maxage=300

4. Cache na Camada de Aplicação (L3)

4.1. Cache em memória: Redis e Memcached

Exemplo de configuração Redis para cache de sessão e consultas:

# Configuração Redis para cache
maxmemory 512mb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
save 60 10000

# Exemplo de cache de consulta frequente
# SETEX user:profile:12345 3600 '{"nome":"João","email":"joao@exemplo.com"}'
# GET user:profile:12345

4.2. Padrões de cache: Cache-Aside, Read-Through, Write-Through

Cache-Aside (Lazy Loading):

1. Tentar ler do cache
2. Se cache miss, ler do banco
3. Armazenar resultado no cache
4. Retornar dados

funcao buscarUsuario(id):
    usuario = redis.get("usuario:" + id)
    se usuario == null:
        usuario = banco.buscarUsuario(id)
        redis.setex("usuario:" + id, 3600, usuario)
    retornar usuario

Write-Through:

1. Escrever no cache
2. Escrever no banco simultaneamente
3. Confirmar escrita

funcao atualizarUsuario(id, dados):
    redis.set("usuario:" + id, dados)
    banco.atualizarUsuario(id, dados)
    retornar sucesso

4.3. Estratégias de expiração: TTL, LRU, LFU e cache warming

# TTL por tipo de dado
# Dados de perfil de usuário: 1 hora (3600s)
# Lista de produtos em destaque: 5 minutos (300s)
# Configurações do sistema: 24 horas (86400s)

# LRU (Least Recently Used) - Redis padrão
maxmemory-policy allkeys-lru

# LFU (Least Frequently Used) - Redis 4.0+
maxmemory-policy allkeys-lfu

# Cache warming em startup
funcao aquecerCache():
    produtos_populares = banco.buscarProdutosPopulares(100)
    para cada produto em produtos_populares:
        redis.setex("produto:" + produto.id, 300, produto)

5. Cache na Camada de Banco de Dados (L4)

5.1. Query cache nativo do banco (MySQL, PostgreSQL) e suas limitações

# MySQL Query Cache (depreciado no MySQL 8.0)
query_cache_type = 1
query_cache_size = 64M
query_cache_limit = 2M

# Limitações:
# - Invalidado a cada modificação na tabela
# - Não funciona com consultas não-determinísticas (NOW(), RAND())
# - Overhead de manutenção para tabelas muito voláteis

# PostgreSQL: shared_buffers e effective_cache_size
shared_buffers = 4GB
effective_cache_size = 12GB

5.2. Buffer pool e índices em memória (InnoDB, Buffer Cache)

# MySQL InnoDB Buffer Pool
innodb_buffer_pool_size = 70% da RAM disponível
innodb_buffer_pool_instances = 4
innodb_old_blocks_time = 1000
innodb_old_blocks_pct = 37

# PostgreSQL Buffer Cache
# shared_buffers: 25% da RAM
# effective_cache_size: 50-75% da RAM
# work_mem: 4-16MB por operação de sort

5.3. Cache de resultados agregados e materialized views

-- Materialized View para relatórios de vendas
CREATE MATERIALIZED VIEW vendas_diarias AS
SELECT 
    DATE(data_venda) as dia,
    COUNT(*) as total_vendas,
    SUM(valor) as receita_total
FROM vendas
GROUP BY DATE(data_venda)
WITH DATA;

-- Refresh periódico
REFRESH MATERIALIZED VIEW CONCURRENTLY vendas_diarias;

-- Índice na materialized view
CREATE INDEX idx_vendas_diarias_dia ON vendas_diarias(dia);

6. Consistência e Coerência entre Camadas

6.1. Problema de coerência de cache: dados obsoletos em múltiplos níveis

Quando um dado é atualizado no banco, as cópias em todas as camadas de cache (L1, L2, L3, L4) podem ficar obsoletas. O desafio é invalidar ou atualizar todas essas cópias de forma coordenada.

6.2. Estratégias de invalidação em cascata: pub/sub com Redis ou mensageria

# Publicador (serviço de atualização)
redis.publish("cache:invalidate", "usuario:12345")

# Assinante (serviço de cache)
redis.subscribe("cache:invalidate", funcao(canal, mensagem):
    chave = mensagem
    redis.del(chave)
    # Opcional: notificar CDN para purgar
    cdn.purge("/api/usuarios/" + extrairId(chave))
)

6.3. Cache stampede: prevenção com mutex locks e cache warming

# Prevenção de cache stampede com mutex lock
funcao buscarDadosCaros(chave):
    dados = redis.get(chave)
    se dados != null:
        retornar dados

    # Tentar adquirir lock
    lock = redis.setnx("lock:" + chave, "1", 10)  # TTL 10s
    se lock:
        # Apenas um processo busca do banco
        dados = banco.buscarDadosCaros()
        redis.setex(chave, 300, dados)
        redis.del("lock:" + chave)
        retornar dados
    senao:
        # Outros processos esperam e tentam novamente
        sleep(50ms)
        retornar buscarDadosCaros(chave)  # Recursão controlada

7. Monitoramento e Otimização do Cache Multicamadas

7.1. Métricas essenciais: hit rate, miss rate, latência e estouro de memória

# Métricas críticas para cada camada:
# L1 (Navegador): cache hit rate > 80%, transfer size
# L2 (CDN): cache hit rate > 90%, bandwidth savings
# L3 (Redis): hit rate > 95%, evicted keys, memory usage
# L4 (Banco): buffer pool hit rate > 99%, query cache hit rate

# Alertas:
# Redis: used_memory > 80% maxmemory
# CDN: cache hit rate < 70%
# Banco: buffer pool hit rate < 95%

7.2. Ferramentas de monitoramento: Prometheus, Grafana e dashboards de cache

# Exemplo de métricas Redis para Prometheus
# redis_info{db="0",role="master"} 1
# redis_keys_total{db="0"} 15000
# redis_hit_rate{db="0"} 0.97
# redis_memory_used_bytes{db="0"} 4294967296

# Dashboard Grafana:
# Painel 1: Hit rate por camada (L1, L2, L3, L4)
# Painel 2: Latência média por camada
# Painel 3: Memória utilizada vs. limite
# Painel 4: Top 10 chaves mais acessadas

7.3. Ajuste dinâmico de TTL e tamanho de cache baseado em padrões de acesso

# Algoritmo de ajuste dinâmico de TTL
funcao ajustarTTL(chave, frequenciaAcesso):
    se frequenciaAcesso > 1000/minuto:
        # Dados muito acessados: TTL longo
        novoTTL = 3600  # 1 hora
    senao se frequenciaAcesso > 100/minuto:
        # Acesso moderado: TTL médio
        novoTTL = 600   # 10 minutos
    senao:
        # Acesso baixo: TTL curto
        novoTTL = 60    # 1 minuto

    redis.expire(chave, novoTTL)

# Ajuste de tamanho de cache baseado em eviction rate
se evicted_keys_por_segundo > 100:
    # Muitas evicções: aumentar memória
    redis.config_set("maxmemory", tamanhoAtual * 1.2)
senao se evicted_keys_por_segundo < 10:
    # Poucas evicções: reduzir memória
    redis.config_set("maxmemory", tamanhoAtual * 0.9)

Referências