Técnicas avançadas de caching para aplicações web

1. Fundamentos e Estratégias de Cache Distribuído

O cache distribuído é a espinha dorsal de aplicações web escaláveis. Ao migrar de um cache local para uma arquitetura em cluster, surgem desafios como consistência de dados e particionamento eficiente.

Redis Cluster vs. Apache Ignite

O Redis Cluster oferece sharding automático com 16384 slots de hash, distribuindo dados entre nós. Cada nó gerencia um subconjunto de slots e replica dados para alta disponibilidade. Já o Apache Ignite utiliza uma arquitetura baseada em memória com suporte a SQL distribuído e computação paralela.

# Exemplo de configuração de Redis Cluster com 3 nós
redis-cli --cluster create 192.168.1.10:6379 192.168.1.11:6379 192.168.1.12:6379 \
  --cluster-replicas 1

Técnicas de Particionamento e Consistência

O particionamento de dados (sharding) pode ser feito por:
- Hash consistente: minimiza remapeamento quando nós são adicionados/removidos
- Range-based: divide dados por faixas de chaves
- Geo-localização: mantém dados próximos aos usuários

A consistência eventual é gerenciada com vetores de relógio (vector clocks) e estratégias de resolução de conflitos.

Gerenciamento de TTL Adaptativo

Políticas de expiração adaptativas ajustam dinamicamente o TTL com base na frequência de acesso:

# Pseudocódigo para TTL adaptativo
def calcular_ttl_adaptativo(frequencia_acesso, ttl_base):
    if frequencia_acesso > 100:
        return ttl_base * 2
    elif frequencia_acesso > 50:
        return ttl_base * 1.5
    else:
        return ttl_base * 0.5

2. Cache em Múltiplas Camadas

Cache no Navegador

Headers HTTP como Cache-Control e ETag controlam o cache do lado do cliente:

# Configuração de cache no servidor (Node.js/Express)
res.setHeader('Cache-Control', 'public, max-age=3600, s-maxage=86400');
res.setHeader('ETag', 'hash-do-conteudo-12345');

Service Workers permitem caching programático offline:

// Service Worker: estratégia Cache First
self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        return response || fetch(event.request);
      })
  );
});

Edge Caching com CDNs

CDNs como Cloudflare e Akamai utilizam cache em pontos de presença (PoPs) globais. Estratégias de purga incluem:
- Purga por URL: invalidação seletiva
- Purga por tag: agrupa recursos relacionados
- Cache warming: pré-carregamento de conteúdo popular

Cache em Camadas L1 e L2

Aplicações modernas implementam cache hierárquico:

# Estrutura de cache em duas camadas
class CacheManager:
    def __init__(self):
        self.l1_cache = {}  # Cache local em memória
        self.l2_cache = RedisClient()  # Cache distribuído

    def get(self, key):
        # Tenta L1 primeiro
        data = self.l1_cache.get(key)
        if data:
            return data

        # Fallback para L2
        data = self.l2_cache.get(key)
        if data:
            self.l1_cache[key] = data  # Popula L1
            return data

        return None

3. Padrões Avançados de Cache

Write-Through

Garante consistência imediata entre banco e cache:

def write_through(usuario):
    # Escreve no banco primeiro
    db.salvar(usuario)
    # Atualiza cache simultaneamente
    cache.set(f"usuario:{usuario.id}", usuario)

Write-Behind (Write-Back)

Otimiza gravações com processamento assíncrono:

def write_behind(usuario):
    # Escreve apenas no cache
    cache.set(f"usuario:{usuario.id}", usuario)
    # Enfileira para gravação no banco
    fila_assincrona.enfileirar(usuario)

Refresh-Ahead

Pré-carrega dados proativamente baseado em padrões de acesso:

def refresh_ahead(key, ttl, threshold):
    # Se o dado está próximo da expiração, atualiza antes
    if cache.ttl(key) < threshold:
        dados = buscar_dados_recentes(key)
        cache.set(key, dados, ttl)

4. Cache de Consultas e Resultados de Banco de Dados

Cache de Queries SQL

Invalidar cache baseado em dependências de tabelas:

# Cache com invalidação por tabela
def cache_query(sql, tabelas_dependentes):
    cache_key = hashlib.md5(sql.encode()).hexdigest()
    resultado = cache.get(cache_key)

    if resultado:
        # Verifica se tabelas foram modificadas
        for tabela in tabelas_dependentes:
            if cache.get(f"ultima_atualizacao:{tabela}") > resultado.timestamp:
                return None  # Cache inválido
        return resultado

    return None

Cache de Objetos ORM

Gerenciamento de identidade com lazy loading:

# Cache de objetos no Hibernate/JPA
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Usuario {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private Departamento departamento;
}

Cache de Agregações Complexas

Materialização de resultados:

# Cache de relatório financeiro agregado
def get_relatorio_vendas(data_inicio, data_fim):
    cache_key = f"relatorio_vendas:{data_inicio}:{data_fim}"
    resultado = cache.get(cache_key)

    if not resultado:
        resultado = db.query("""
            SELECT SUM(valor), COUNT(*) 
            FROM vendas 
            WHERE data BETWEEN :inicio AND :fim
        """)
        cache.set(cache_key, resultado, ttl=3600)

    return resultado

5. Técnicas de Invalidação e Consistência

Invalidação por Evento (Pub/Sub)

Padrão Cache-Aside com notificações:

# Publica evento de invalidação
def atualizar_usuario(usuario_id, dados):
    db.atualizar(usuario_id, dados)
    redis.publish("cache_invalidation", f"usuario:{usuario_id}")

Invalidação Baseada em Versão

Uso de timestamps e vetores de relógio:

def get_com_versao(key):
    versao = cache.get(f"versao:{key}")
    dados = cache.get(key)

    if dados and dados.versao == versao:
        return dados
    return None

Cache Tolerante a Falhas

Degradação graciosa quando cache falha:

def get_com_fallback(key):
    try:
        return cache.get(key)
    except ConnectionError:
        # Fallback para banco de dados
        return db.get(key)
    except Exception:
        # Fallback para cache local
        return local_cache.get(key)

6. Cache de Sessão e Autenticação Distribuída

Sessões Centralizadas vs. Stateless

Sessões em Redis:

# Armazenamento de sessão em Redis
redis.setex(f"sessao:{session_id}", 3600, {
    "usuario_id": 123,
    "permissoes": ["admin", "editor"]
})

JWT para sessões stateless:

# Token JWT com claims de autorização
{
  "sub": "123",
  "exp": 1700000000,
  "permissoes": ["admin", "editor"],
  "iat": 1699996400
}

Rate Limiting com Contadores Atômicos

def rate_limit(usuario_id, limite=100, janela=60):
    chave = f"rate_limit:{usuario_id}:{int(time.time()/janela)}"
    contagem = redis.incr(chave)
    if contagem == 1:
        redis.expire(chave, janela)
    return contagem <= limite

7. Monitoramento, Métricas e Otimização

Métricas-Chave

  • Hit ratio: percentual de acessos bem-sucedidos ao cache
  • Miss ratio: acessos que falharam e foram ao backend
  • Latência: tempo de resposta do cache
  • Taxa de expiração: itens expirados vs. removidos manualmente

Ferramentas de Monitoramento

# Configuração de métricas com Prometheus
redis_hits_total = Counter('redis_hits_total', 'Total de hits no cache')
redis_misses_total = Counter('redis_misses_total', 'Total de misses')
cache_latency = Histogram('cache_latency_seconds', 'Latência do cache')

Técnicas de Tuning

Políticas de evicção:
- LRU (Least Recently Used): remove itens menos acessados recentemente
- LFU (Least Frequently Used): remove itens menos acessados no total
- TTL: remove itens expirados

# Ajuste de política de evicção no Redis
CONFIG SET maxmemory-policy allkeys-lru
CONFIG SET maxmemory 2gb

Referências