Dominando Cache e Performance em Projetos Reais Já leu

Cache: Fundamentos e Estratégias Práticas Cache é um mecanismo de armazenamento rápido que reduz a latência de acesso a dados frequentemente utilizados. Em projetos reais, implementar cache corretamente pode reduzir o tempo de resposta em até 10x e diminuir drasticamente a carga no banco de dados. Existem três níveis principais: cache em memória (Redis, Memcached), cache de aplicação (em-process) e cache de HTTP (navegador e CDN). A escolha entre cache local e distribuído depende da arquitetura. Para aplicações monolíticas, cache em memória é suficiente. Para microserviços, Redis é o padrão. Vamos implementar um exemplo prático em Python com Redis: Este exemplo demonstra o padrão Cache-Aside, onde a aplicação é responsável por gerenciar o cache. A estratégia TTL (Time To Live) garante que dados obsoletos sejam automaticamente removidos. Padrões de Cache e Invalidação Existem diferentes estratégias de cache, cada uma adequada para cenários específicos. O padrão Cache-Aside (mostrado acima) é versátil mas exige lógica na aplicação. O padrão Write-Through escreve no

Cache: Fundamentos e Estratégias Práticas

Cache é um mecanismo de armazenamento rápido que reduz a latência de acesso a dados frequentemente utilizados. Em projetos reais, implementar cache corretamente pode reduzir o tempo de resposta em até 10x e diminuir drasticamente a carga no banco de dados. Existem três níveis principais: cache em memória (Redis, Memcached), cache de aplicação (em-process) e cache de HTTP (navegador e CDN).

A escolha entre cache local e distribuído depende da arquitetura. Para aplicações monolíticas, cache em memória é suficiente. Para microserviços, Redis é o padrão. Vamos implementar um exemplo prático em Python com Redis:

import redis
import json
from datetime import timedelta

class UserCache:
    def __init__(self):
        self.redis_client = redis.Redis(host='localhost', port=6379, decode_responses=True)
        self.ttl = 3600  # 1 hora

    def get_user(self, user_id):
        cache_key = f"user:{user_id}"
        cached = self.redis_client.get(cache_key)

        if cached:
            return json.loads(cached)

        # Simular busca no banco de dados
        user_data = {"id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com"}
        self.redis_client.setex(cache_key, self.ttl, json.dumps(user_data))
        return user_data

    def invalidate_user(self, user_id):
        self.redis_client.delete(f"user:{user_id}")

# Uso
cache = UserCache()
print(cache.get_user(1))  # Busca no BD e cacheia
print(cache.get_user(1))  # Retorna do cache

Este exemplo demonstra o padrão Cache-Aside, onde a aplicação é responsável por gerenciar o cache. A estratégia TTL (Time To Live) garante que dados obsoletos sejam automaticamente removidos.

Padrões de Cache e Invalidação

Existem diferentes estratégias de cache, cada uma adequada para cenários específicos. O padrão Cache-Aside (mostrado acima) é versátil mas exige lógica na aplicação. O padrão Write-Through escreve no cache e no banco simultaneamente, garantindo consistência mas sacrificando velocidade de escrita. O padrão Write-Behind (Write-Back) escreve apenas no cache e sincroniza com o banco posteriormente, maximizando performance mas com risco de perda de dados.

A invalidação é crítica: dados desatualizados causam bugs severos. Existem três estratégias principais. TTL baseado em tempo é simples mas pode servir dados antigos. Invalidação explícita é precisa mas requer código para detectar mudanças. Invalidação baseada em eventos (usando message brokers como Kafka) é robusta para aplicações distribuídas. Aqui está um exemplo com invalidação por eventos em Node.js:

const redis = require('redis');
const EventEmitter = require('events');

class ProductCache extends EventEmitter {
    constructor() {
        super();
        this.client = redis.createClient({ host: 'localhost', port: 6379 });
        this.setupInvalidationListener();
    }

    getProduct(productId) {
        return new Promise((resolve) => {
            this.client.get(`product:${productId}`, (err, data) => {
                if (data) {
                    resolve(JSON.parse(data));
                } else {
                    // Simular busca no BD
                    const product = { id: productId, name: `Product ${productId}`, price: 99.99 };
                    this.client.setex(`product:${productId}`, 3600, JSON.stringify(product));
                    resolve(product);
                }
            });
        });
    }

    setupInvalidationListener() {
        this.on('product:updated', (productId) => {
            this.client.del(`product:${productId}`);
            console.log(`Cache invalidado para produto ${productId}`);
        });
    }
}

const cache = new ProductCache();
cache.getProduct(1).then(p => console.log(p));
cache.emit('product:updated', 1);  // Invalida o cache

Medição de Performance e Otimizações Avançadas

Não há otimização sem medição. Ferramentas como New Relic, Datadog e mesmo logs estruturados em JSON permitem quantificar o impacto do cache. Métricas essenciais incluem cache hit ratio (% de requisições servidas do cache), latência p50/p95/p99 e throughput. Um hit ratio abaixo de 60% indica revisão da estratégia.

Além de cache tradicional, otimizações avançadas incluem lazy loading (carregar dados sob demanda), prefetching (antecipar carregamentos) e compression (reduzir tamanho dos dados). Um exemplo prático em Java com Spring Cache e compressão:

import org.springframework.cache.annotation.Cacheable;
import java.io.IOException;
import java.util.zip.GZIPOutputStream;
import java.io.ByteArrayOutputStream;

@Service
public class ReportService {

    @Cacheable(value = "reports", key = "#reportId", unless = "#result == null")
    public String getReport(String reportId) {
        // Simular geração de relatório pesado
        String largeReport = generateLargeReport(reportId);
        return compress(largeReport);
    }

    private String compress(String data) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            GZIPOutputStream gzip = new GZIPOutputStream(baos);
            gzip.write(data.getBytes());
            gzip.close();
            return Base64.getEncoder().encodeToString(baos.toByteArray());
        } catch (IOException e) {
            return data;
        }
    }

    private String generateLargeReport(String reportId) {
        // Simula processamento pesado
        return "Report data for " + reportId + " with heavy computation...";
    }
}

Utilize conexão de pool para Redis/Memcached, defina políticas de eviction apropriadas (LRU, LFU) e monitore o uso de memória constantemente. Em produção, um cache mal configurado é pior que nenhum cache.

Conclusão

Dominando cache, você elimina gargalos e escala sistemas de forma elegante. Retenha três pontos: (1) escolha a estratégia de cache (aside, write-through, write-behind) conforme sua arquitetura e requisitos de consistência; (2) implemente invalidação robusta — com TTL para dados menos críticos e eventos para dados críticos; (3) meça sempre — hit ratio, latência e throughput revelam se o cache realmente vale. Cache não é opcional em aplicações modernas: é fundamental.

Referências


Artigos relacionados