Cloud e plataforma

Padrões de Cache Distribuído: Estratégias para Escala e Consistência

Cache é a diferença entre um sistema que escala e um que colapsa sob carga. Aprenda padrões write-through, write-behind, read-through e invalidação de cache para sistemas distribuídos.

11/03/20269 min de leituraCloud
Padrões de Cache Distribuído: Estratégias para Escala e Consistência

Resumo executivo

Cache é a diferença entre um sistema que escala e um que colapsa sob carga. Aprenda padrões write-through, write-behind, read-through e invalidação de cache para sistemas distribuídos.

Ultima atualizacao: 11/03/2026

O problema de cache

Em escala, cada query de banco de dados se torna um gargalo. A história é familiar: aplicação lança, tráfego cresce, pools de conexão de banco esgotam, tempos de resposta disparam, e o sistema colapsa sob carga. Cache resolve isso armazenando dados frequentemente acessados em memória, reduzindo drasticamente a carga do banco de dados.

O desafio é implementar cache corretamente. Caches mal projetados introduzem dados obsoletos, estado inconsistente e complexidade que anula ganhos de performance. A diferença entre um sistema que escala e um que introduz bugs está em entender padrões de cache: quando escrever, o que invalidar, e como lidar com misses.

Cache efetivo em sistemas distribuídos requer escolher o padrão certo para seu caso de uso: cache-aside para workloads leitura-intensivas, write-through para forte consistência, write-behind para throughput de escrita, e read-through para lógica de aplicação simplificada.

Padrão cache-aside

O padrão cache-aside (também chamado de lazy loading) verifica o cache primeiro, então retorna ao banco de dados em miss, populando o cache para requisições subsequentes.

Quando usar cache-aside

  • Workloads leitura-intensivas com acesso frequente aos mesmos dados
  • Dados que mudam infrequentemente
  • Quando você quer cache populado apenas sob demanda
  • Quando redução de carga do banco de dados é o objetivo principal

Implementação

typescriptclass ServicoCacheAside {
  constructor(
    private cache: RedisClient,
    private database: Database
  ) {}

  async getUsuario(usuarioId: number): Promise<Usuario> {
    // Passo 1: Verificar cache
    const cacheKey = `usuario:${usuarioId}`;
    const cached = await this.cache.get(cacheKey);

    if (cached) {
      this.metrics.record('cache_hit', { key: cacheKey });
      return JSON.parse(cached);
    }

    // Passo 2: Cache miss - buscar do banco de dados
    this.metrics.record('cache_miss', { key: cacheKey });
    const usuario = await this.database.usuarios.findUnique({
      where: { id: usuarioId }
    });

    if (!usuario) {
      throw new NotFoundError(`Usuario ${usuarioId} não encontrado`);
    }

    // Passo 3: Popular cache para requisições futuras
    await this.cache.setex(cacheKey, 3600, JSON.stringify(usuario));

    return usuario;
  }

  async atualizarUsuario(usuarioId: number, dados: Partial<DadosUsuario>): Promise<Usuario> {
    // Passo 1: Atualizar banco de dados
    const usuario = await this.database.usuarios.update({
      where: { id: usuarioId },
      dados
    });

    // Passo 2: Invalidar cache
    const cacheKey = `usuario:${usuarioId}`;
    await this.cache.del(cacheKey);
    this.metrics.record('invalidacao_cache', { key: cacheKey });

    // Próxima leitura repopulará cache com dados frescos
    return usuario;
  }
}

Vantagens e trade-offs

VantagensTrade-offs
Cache populado apenas sob demandaPrimeira requisição sofre de latência de cache miss
Implementação simplesDados obsoletos possíveis entre invalidação e repopulação
Funciona bem para workloads leitura-intensivasThundering herd se cache expira e múltiplas requisições atingem simultaneamente

Mitigando thundering herd

typescriptasync getUsuarioComLock(usuarioId: number): Promise<Usuario> {
  const cacheKey = `usuario:${usuarioId}`;
  const lockKey = `lock:${cacheKey}`;

  // Verificar cache primeiro
  const cached = await this.cache.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }

  // Adquirir lock para prevenir thundering herd
  const lockAdquirido = await this.cache.set(lockKey, '1', {
    NX: true,
    EX: 10 // timeout de lock de 10 segundos
  });

  if (lockAdquirido) {
    try {
      // Temos o lock - buscar do banco de dados
      const usuario = await this.database.usuarios.findUnique({
        where: { id: usuarioId }
      });

      if (usuario) {
        await this.cache.setex(cacheKey, 3600, JSON.stringify(usuario));
      }

      return usuario;
    } finally {
      // Liberar lock
      await this.cache.del(lockKey);
    }
  } else {
    // Esperar por detentor do lock e tentar novamente
    await sleep(100);
    return this.getUsuario(usuarioId);
  }
}

Padrão write-through

O padrão write-through escreve em ambos cache e banco de dados sincronamente. O cache é sempre consistente com o banco de dados.

Quando usar write-through

  • Quando você precisa de forte consistência entre cache e banco de dados
  • Quando latência de leitura é mais importante que latência de escrita
  • Quando cache misses são caros e devem ser minimizados
  • Quando você pode permitir operações de escrita ligeiramente mais lentas

Implementação

typescriptclass ServicoCacheWriteThrough {
  async criarUsuario(dados: DadosUsuario): Promise<Usuario> {
    // Escrever no banco de dados primeiro
    const usuario = await this.database.usuarios.create({
      dados
    });

    // Imediatamente escrever no cache
    const cacheKey = `usuario:${usuario.id}`;
    await this.cache.setex(cacheKey, 3600, JSON.stringify(usuario));
    this.metrics.record('write_through', { key: cacheKey });

    return usuario;
  }

  async atualizarUsuario(usuarioId: number, dados: Partial<DadosUsuario>): Promise<Usuario> {
    // Atualizar no banco de dados
    const usuario = await this.database.usuarios.update({
      where: { id: usuarioId },
      dados
    });

    // Atualizar no cache
    const cacheKey = `usuario:${usuarioId}`;
    await this.cache.setex(cacheKey, 3600, JSON.stringify(usuario));
    this.metrics.record('write_through', { key: cacheKey });

    return usuario;
  }

  async getUsuario(usuarioId: number): Promise<Usuario> {
    const cacheKey = `usuario:${usuarioId}`;
    const cached = await this.cache.get(cacheKey);

    if (cached) {
      this.metrics.record('cache_hit', { key: cacheKey });
      return JSON.parse(cached);
    }

    // Cache miss - não deveria acontecer com write-through
    // mas tratar graciosamente para falhas de cache
    const usuario = await this.database.usuarios.findUnique({
      where: { id: usuarioId }
    });

    if (usuario) {
      await this.cache.setex(cacheKey, 3600, JSON.stringify(usuario));
    }

    return usuario;
  }
}

Vantagens e trade-offs

VantagensTrade-offs
Forte consistência entre cache e banco de dadosOperações de escrita mais lentas (duas escritas)
Cache sempre contém dados mais recentesMaior latência em escritas
Sem thundering herd em cache missEscritas desnecessárias para dados que não serão lidos

Padrão write-behind

O padrão write-behind (também chamado write-back) escreve no cache primeiro e persiste assincronamente no banco de dados. Isso maximiza throughput de escrita.

Quando usar write-behind

  • Workloads escrita-intensivas requerendo alto throughput
  • Quando consistência eventual é aceitável
  • Quando você pode tolerar potencial perda de dados em falha de cache
  • Para escritas de alto volume que não precisam de persistência imediata

Implementação

typescriptclass ServicoCacheWriteBehind {
  private filaEscrita: WriteQueue;

  async atualizarUsuario(usuarioId: number, dados: Partial<DadosUsuario>): Promise<Usuario> {
    const cacheKey = `usuario:${usuarioId}`;

    // Atualizar no cache imediatamente (escrita rápida)
    const atual = await this.cache.get(cacheKey);
    const usuario = atual ? JSON.parse(atual) : {};
    const atualizado = { ...usuario, ...dados };

    await this.cache.setex(cacheKey, 3600, JSON.stringify(atualizado));

    // Enfileirar para escrita assíncrona no banco de dados
    await this.filaEscrita.push({
      tipo: 'ATUALIZAR_USUARIO',
      usuarioId,
      dados,
      timestamp: Date.now()
    });

    return atualizado;
  }

  async iniciarWorkerEscrita(): Promise<void> {
    setInterval(async () => {
      const batch = await this.filaEscrita.getBatch(100); // Batch até 100 itens

      for (const item of batch) {
        try {
          await this.processarItemEscrita(item);
        } catch (error) {
          this.logger.error('Write-behind falhou', { error, item });
          // Retentar ou mover para fila de dead letter
        }
      }
    }, 1000); // Processar a cada segundo
  }

  private async processarItemEscrita(item: WriteQueueItem): Promise<void> {
    switch (item.tipo) {
      case 'ATUALIZAR_USUARIO':
        await this.database.usuarios.update({
          where: { id: item.usuarioId },
          dados: item.dados
        });
        break;
      // Tratar outros tipos de escrita
    }
  }
}

Vantagens e trade-offs

VantagensTrade-offs
Máximo throughput de escritaPerda de dados possível se cache falhar antes de persistir
Tempo de resposta rápido para escritasRecuperação e tratamento de consistência complexos
Batching reduz carga do banco de dadosConsistência eventual
Reduz amplificação de escritaDifícil de implementar corretamente

Padrão read-through

O padrão read-through abstrai lógica de cache atrás de uma interface simples. O cache lida com misses automaticamente buscando do banco de dados.

Quando usar read-through

  • Quando você quer simplificar código da aplicação
  • Quando lógica de cache deve ser centralizada
  • Quando você tem muitas buscas de cache similares
  • Quando você quer comportamento de cache consistente através da codebase

Implementação

typescriptclass CacheReadThrough<T> {
  constructor(
    private cache: RedisClient,
    private loader: (key: string) => Promise<T>,
    private ttl: number = 3600
  ) {}

  async get(key: string): Promise<T> {
    const cached = await this.cache.get(key);

    if (cached) {
      this.metrics.record('cache_hit', { key });
      return JSON.parse(cached);
    }

    // Cache miss - usar loader
    this.metrics.record('cache_miss', { key });
    const value = await this.loader(key);

    // Armazenar no cache
    await this.cache.setex(key, this.ttl, JSON.stringify(value));

    return value;
  }

  async invalidate(key: string): Promise<void> {
    await this.cache.del(key);
    this.metrics.record('invalidacao_cache', { key });
  }
}

// Uso
class ServicoUsuario {
  private cacheUsuario: CacheReadThrough<Usuario>;

  constructor(cache: RedisClient, database: Database) {
    this.cacheUsuario = new CacheReadThrough(
      cache,
      async (key) => {
        const usuarioId = parseInt(key.split(':')[1]);
        return database.usuarios.findUnique({ where: { id: usuarioId } });
      },
      3600 // TTL de 1 hora
    );
  }

  async getUsuario(usuarioId: number): Promise<Usuario> {
    return this.cacheUsuario.get(`usuario:${usuarioId}`);
  }

  async atualizarUsuario(usuarioId: number, dados: Partial<DadosUsuario>): Promise<Usuario> {
    const usuario = await this.database.usuarios.update({
      where: { id: usuarioId },
      dados
    });

    await this.cacheUsuario.invalidate(`usuario:${usuarioId}`);
    return usuario;
  }
}

Estratégias de invalidação de cache

A parte mais difícil de cache é saber quando invalidar dados em cache.

Expiração baseada em tempo

typescript// Definir TTL em chaves de cache
await this.cache.setex(`usuario:${usuarioId}`, 3600, JSON.stringify(usuario)); // 1 hora

// Configurar diferentes TTLs baseado em volatilidade de dados
const configTTL = {
  estatico: 86400,      // 24 horas
  frequente: 3600,       // 1 hora
  volatil: 300,          // 5 minutos
  sessao: 1800           // 30 minutos
};

Invalidação baseada em eventos

typescriptclass ServicoInvalidacaoCache {
  async invalidarRelacionadoUsuario(usuarioId: number): Promise<void> {
    // Invalidar cache direto de usuário
    await this.cache.del(`usuario:${usuarioId}`);

    // Invalidar caches relacionados
    const postsUsuario = await this.database.posts.findMany({
      where: { usuarioId },
      select: { id: true }
    });

    for (const post of postsUsuario) {
      await this.cache.del(`post:${post.id}`);
    }

    // Invalidar cache de feed do usuário
    await this.cache.del(`feed:${usuarioId}`);

    this.metrics.record('invalidacao_cascata', {
      usuarioId,
      itensInvalidados: postsUsuario.length + 2
    });
  }
}

Invalidação baseada em tags

typescriptclass CacheTagged {
  async set(key: string, value: any, tags: string[], ttl: number): Promise<void> {
    await this.cache.setex(key, ttl, JSON.stringify(value));

    // Rastrear tags para esta chave
    for (const tag of tags) {
      await this.cache.sadd(`tags:${tag}`, key);
    }
  }

  async invalidarTag(tag: string): Promise<void> {
    // Obter todas as chaves com esta tag
    const keys = await this.cache.smembers(`tags:${tag}`);

    // Deletar todas as chaves taggeadas
    if (keys.length > 0) {
      await this.cache.del(...keys);
    }

    // Limpar conjunto de tags
    await this.cache.del(`tags:${tag}`);
  }
}

// Uso
await cacheTagged.set('usuario:123', dadosUsuario, ['usuario', 'frequente'], 3600);
await cacheTagged.set('post:456', dadosPost, ['post', 'volatil'], 300);

// Invalidar todos os caches relacionados a usuário
await cacheTagged.invalidarTag('usuario');

Consistência de cache distribuído

Em sistemas distribuídos com múltiplos nós de cache, manter consistência se torna desafiador.

Redis Cluster

typescriptclass CacheRedisCluster {
  private client: RedisCluster;

  async get(key: string): Promise<string | null> {
    return this.client.get(key);
  }

  async set(key: string, value: string, opcoes: { ttl?: number }): Promise<void> {
    if (opcoes.ttl) {
      await this.client.setex(key, opcoes.ttl, value);
    } else {
      await this.client.set(key, value);
    }
  }
}

Cache warming

typescriptclass ServicoCacheWarmup {
  async warmCacheUsuario(usuarioIds: number[]): Promise<void> {
    const tamanhoLote = 100;

    for (let i = 0; i < usuarioIds.length; i += tamanhoLote) {
      const lote = usuarioIds.slice(i, i + tamanhoLote);

      await Promise.all(
        lote.map(async (usuarioId) => {
          const usuario = await this.database.usuarios.findUnique({
            where: { id: usuarioId }
          });

          if (usuario) {
            await this.cache.setex(
              `usuario:${usuarioId}`,
              3600,
              JSON.stringify(usuario)
            );
          }
        })
      );
    }
  }
}

Anti-padrões comuns de cache

Anti-padrão 1: Cachear tudo

Problema: Cachear todos os dados independentemente do padrão de acesso.

Consequências: Memória de cache esgotada, baixas taxas de hit, performance degradada.

Solução: Cachear apenas dados frequentemente acessados com custo de recuperação significativo.

Anti-padrão 2: Sem estratégia de invalidação de cache

Problema: Definir TTL muito longo ou nunca invalidar cache.

Consequências: Dados obsoletos se propagam através do sistema, estado inconsistente.

Solução: Implementar expiração baseada em tempo mais invalidação baseada em eventos para dados críticos.

Anti-padrão 3: Padrão de query N+1 com cache

Problema: Cachear itens individuais em um loop.

typescript// RUIM
for (const usuarioId of usuarioIds) {
  const usuario = await getUsuario(usuarioId); // Cada chamada verifica cache individualmente
  usuarios.push(usuario);
}

// BOM
const keys = usuarioIds.map(id => `usuario:${id}`);
const cached = await this.cache.mget(...keys);
// Então buscar apenas usuários ausentes do banco de dados

Anti-padrão 4: Ignorar falhas de cache

Problema: Não tratar falhas de cache graciosamente.

Consequências: Aplicação quebra quando cache está indisponível.

Solução: Implementar fallback para banco de dados quando cache falha.

typescriptasync getComFallback(key: string): Promise<any> {
  try {
    const cached = await this.cache.get(key);
    if (cached) return JSON.parse(cached);
  } catch (error) {
    this.logger.warn('Cache indisponível, usando fallback para banco de dados', { error });
  }

  return this.database.query(...);
}

Seleção de tecnologia de cache

TecnologiaMelhor paraTrade-offs
RedisEstruturas de dados complexas, persistênciaSingle-threaded, intensivo em memória
MemcachedSimples chave-valor, alto throughputSem persistência, tipos de dados limitados
HazelcastComputação distribuída, data grid in-memorySetup complexo, footprint mais pesado
IgniteSQL-on-memory, transações distribuídasCurva de aprendizado mais íngreme

Monitoramento e métricas

Rastreie eficácia de cache para otimizar sua estratégia.

typescriptinterface MetricasCache {
  taxaHit: number;
  taxaMiss: number;
  latenciaMediaHit: number;
  latenciaMediaMiss: number;
  qtdEvicoes: number;
  usoMemoria: number;
}

async getMetricasCache(): Promise<MetricasCache> {
  const hits = await this.metrics.getCounter('cache_hit');
  const misses = await this.metrics.getCounter('cache_miss');
  const total = hits + misses;

  return {
    taxaHit: total > 0 ? hits / total : 0,
    taxaMiss: total > 0 ? misses / total : 0,
    latenciaMediaHit: await this.metrics.getAverage('cache_hit_latency'),
    latenciaMediaMiss: await this.metrics.getAverage('cache_miss_latency'),
    qtdEvicoes: await this.cache.dbsize(),
    usoMemoria: await this.cache.info('memory').then(info => info.used_memory)
  };
}

Conclusão

Padrões de cache distribuído fornecem um kit de ferramentas para construir sistemas escaláveis. Cache-aside se adequa a workloads leitura-intensivas, write-through garante consistência, write-behind maximiza throughput, e read-through simplifica lógica de aplicação.

A chave é combinar o padrão ao seu caso de uso. Não cacheie tudo—focar em dados frequentemente acessados e caros de recuperar. Implemente invalidação apropriada para prevenir dados obsoletos. Monitore métricas de cache para ajustar TTL e identificar padrões.

Comece com cache-aside para caminhos leitura-intensivos. Adicione write-through onde consistência importa. Considere write-behind para workloads escrita-intensivas aceitando consistência eventual. Sempre trate falhas de cache graciosamente retornando ao banco de dados.


Seu sistema precisa de uma estratégia de cache que escale com crescimento? Fale com especialistas em engenharia da Imperialis sobre implementação de cache distribuído, seleção de padrão e otimização de performance para sua arquitetura de produção.

Fontes

Leituras relacionadas