Guia Prático de Cache com Redis - Design e Implementação de Acesso Rápido a Dados

Intermediário | 2025.12.02

Redis é um data store in-memory de alta velocidade, amplamente utilizado para cache, gerenciamento de sessões, funcionalidades em tempo real e muito mais. Neste artigo, explicamos de forma prática estratégias eficazes de cache usando Redis.

Fundamentos do Redis

Configuração de Conexão

// redis.ts
import Redis from 'ioredis';

// Instância única
const redis = new Redis({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  password: process.env.REDIS_PASSWORD,
  db: 0,
  retryStrategy: (times) => {
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
  maxRetriesPerRequest: 3,
  enableReadyCheck: true,
  lazyConnect: true,
});

// Configuração de cluster
const cluster = new Redis.Cluster([
  { host: 'redis-node-1', port: 6379 },
  { host: 'redis-node-2', port: 6379 },
  { host: 'redis-node-3', port: 6379 },
], {
  redisOptions: {
    password: process.env.REDIS_PASSWORD,
  },
  scaleReads: 'slave', // Distribuir leituras para slaves
  maxRedirections: 16,
});

// Eventos de conexão
redis.on('connect', () => console.log('Redis connected'));
redis.on('error', (err) => console.error('Redis error:', err));
redis.on('close', () => console.log('Redis connection closed'));

export { redis, cluster };

Visão Geral dos Tipos de Dados

Tipo de DadoUso
StringCache, contadores, sessões
HashArmazenamento de objetos, perfis de usuário
ListFilas, timelines, logs
SetTags, valores únicos, relacionamentos
Sorted SetRankings, scoreboards, timelines
StreamEvent sourcing, mensageria
JSONObjetos complexos (RedisJSON)

Padrões de Cache

Padrão Cache-Aside

// cache-aside.ts
import { redis } from './redis';

interface CacheOptions {
  ttl?: number; // segundos
  prefix?: string;
}

class CacheAside<T> {
  private prefix: string;
  private defaultTtl: number;

  constructor(prefix: string, defaultTtl: number = 3600) {
    this.prefix = prefix;
    this.defaultTtl = defaultTtl;
  }

  private getKey(id: string): string {
    return `${this.prefix}:${id}`;
  }

  async get(id: string): Promise<T | null> {
    const cached = await redis.get(this.getKey(id));
    if (cached) {
      return JSON.parse(cached);
    }
    return null;
  }

  async set(id: string, data: T, ttl?: number): Promise<void> {
    const key = this.getKey(id);
    const serialized = JSON.stringify(data);
    await redis.setex(key, ttl || this.defaultTtl, serialized);
  }

  async getOrFetch(
    id: string,
    fetcher: () => Promise<T | null>,
    ttl?: number
  ): Promise<T | null> {
    // 1. Verificar cache
    const cached = await this.get(id);
    if (cached) {
      return cached;
    }

    // 2. Buscar da fonte de dados
    const data = await fetcher();
    if (data === null) {
      return null;
    }

    // 3. Salvar no cache
    await this.set(id, data, ttl);

    return data;
  }

  async invalidate(id: string): Promise<void> {
    await redis.del(this.getKey(id));
  }

  async invalidatePattern(pattern: string): Promise<void> {
    const keys = await redis.keys(`${this.prefix}:${pattern}`);
    if (keys.length > 0) {
      await redis.del(...keys);
    }
  }
}

// Exemplo de uso
interface User {
  id: string;
  name: string;
  email: string;
}

const userCache = new CacheAside<User>('user', 3600);

async function getUser(id: string): Promise<User | null> {
  return userCache.getOrFetch(id, async () => {
    // Buscar do banco de dados
    const user = await db.user.findUnique({ where: { id } });
    return user;
  });
}

async function updateUser(id: string, data: Partial<User>): Promise<User> {
  // Atualizar banco de dados
  const user = await db.user.update({
    where: { id },
    data,
  });

  // Invalidar cache
  await userCache.invalidate(id);

  return user;
}

Padrão Write-Through

// write-through.ts

class WriteThrough<T extends { id: string }> {
  private cache: CacheAside<T>;

  constructor(
    prefix: string,
    private repository: Repository<T>,
    ttl: number = 3600
  ) {
    this.cache = new CacheAside<T>(prefix, ttl);
  }

  async get(id: string): Promise<T | null> {
    return this.cache.getOrFetch(id, () => this.repository.findById(id));
  }

  async create(data: Omit<T, 'id'>): Promise<T> {
    // 1. Escrever no banco de dados
    const entity = await this.repository.create(data);

    // 2. Escrever no cache simultaneamente
    await this.cache.set(entity.id, entity);

    return entity;
  }

  async update(id: string, data: Partial<T>): Promise<T> {
    // 1. Atualizar banco de dados
    const entity = await this.repository.update(id, data);

    // 2. Atualizar cache
    await this.cache.set(id, entity);

    return entity;
  }

  async delete(id: string): Promise<void> {
    // 1. Deletar do banco de dados
    await this.repository.delete(id);

    // 2. Deletar do cache
    await this.cache.invalidate(id);
  }
}

Padrão Write-Behind

// write-behind.ts
import { redis } from './redis';

class WriteBehind<T extends { id: string }> {
  private pendingWrites: Map<string, T> = new Map();
  private flushInterval: NodeJS.Timeout;

  constructor(
    private prefix: string,
    private repository: Repository<T>,
    private batchSize: number = 100,
    private flushIntervalMs: number = 5000
  ) {
    // Escrita em lote periodicamente
    this.flushInterval = setInterval(
      () => this.flush(),
      flushIntervalMs
    );
  }

  async write(entity: T): Promise<void> {
    // 1. Escrever no cache imediatamente
    await redis.setex(
      `${this.prefix}:${entity.id}`,
      3600,
      JSON.stringify(entity)
    );

    // 2. Adicionar à fila pendente
    this.pendingWrites.set(entity.id, entity);

    // Flush quando atingir tamanho do lote
    if (this.pendingWrites.size >= this.batchSize) {
      await this.flush();
    }
  }

  async get(id: string): Promise<T | null> {
    // Se estiver pendente, retornar isso
    if (this.pendingWrites.has(id)) {
      return this.pendingWrites.get(id)!;
    }

    // Verificar cache
    const cached = await redis.get(`${this.prefix}:${id}`);
    if (cached) {
      return JSON.parse(cached);
    }

    // Buscar do banco de dados
    return this.repository.findById(id);
  }

  private async flush(): Promise<void> {
    if (this.pendingWrites.size === 0) return;

    const writes = Array.from(this.pendingWrites.values());
    this.pendingWrites.clear();

    try {
      // Escrever em lote no banco de dados
      await this.repository.upsertMany(writes);
      console.log(`Flushed ${writes.length} writes to database`);
    } catch (error) {
      // Em caso de falha, retornar para pendente
      for (const entity of writes) {
        this.pendingWrites.set(entity.id, entity);
      }
      console.error('Flush failed:', error);
    }
  }

  async stop(): Promise<void> {
    clearInterval(this.flushInterval);
    await this.flush();
  }
}

Estratégias de TTL

TTL Adaptativo

// adaptive-ttl.ts

interface CacheMetrics {
  hits: number;
  misses: number;
  lastAccess: number;
}

class AdaptiveTTLCache<T> {
  private metrics: Map<string, CacheMetrics> = new Map();
  private readonly minTtl: number;
  private readonly maxTtl: number;
  private readonly baseTtl: number;

  constructor(
    private prefix: string,
    options: { minTtl?: number; maxTtl?: number; baseTtl?: number } = {}
  ) {
    this.minTtl = options.minTtl || 60;      // 1 minuto
    this.maxTtl = options.maxTtl || 86400;   // 24 horas
    this.baseTtl = options.baseTtl || 3600;  // 1 hora
  }

  private calculateTtl(key: string): number {
    const metric = this.metrics.get(key);

    if (!metric) {
      return this.baseTtl;
    }

    const hitRate = metric.hits / (metric.hits + metric.misses + 1);
    const timeSinceLastAccess = Date.now() - metric.lastAccess;

    // Quanto maior a taxa de hit, maior o TTL
    let ttl = this.baseTtl * (1 + hitRate);

    // Quanto maior a frequência de acesso, maior o TTL
    if (timeSinceLastAccess < 60000) { // Menos de 1 minuto
      ttl *= 1.5;
    }

    return Math.min(Math.max(ttl, this.minTtl), this.maxTtl);
  }

  async get(key: string): Promise<T | null> {
    const fullKey = `${this.prefix}:${key}`;
    const cached = await redis.get(fullKey);

    const metric = this.metrics.get(key) || { hits: 0, misses: 0, lastAccess: 0 };

    if (cached) {
      metric.hits++;
      metric.lastAccess = Date.now();
      this.metrics.set(key, metric);
      return JSON.parse(cached);
    }

    metric.misses++;
    this.metrics.set(key, metric);
    return null;
  }

  async set(key: string, data: T): Promise<void> {
    const fullKey = `${this.prefix}:${key}`;
    const ttl = Math.round(this.calculateTtl(key));

    await redis.setex(fullKey, ttl, JSON.stringify(data));
  }
}

Cache em Camadas

// tiered-cache.ts

class TieredCache<T> {
  private l1: Map<string, { data: T; expiry: number }> = new Map();
  private l1MaxSize: number;
  private l1Ttl: number;

  constructor(
    private prefix: string,
    private l2Ttl: number = 3600,
    l1Options: { maxSize?: number; ttl?: number } = {}
  ) {
    this.l1MaxSize = l1Options.maxSize || 1000;
    this.l1Ttl = l1Options.ttl || 60;
  }

  async get(key: string): Promise<T | null> {
    // Verificar L1 (in-memory)
    const l1Entry = this.l1.get(key);
    if (l1Entry && l1Entry.expiry > Date.now()) {
      return l1Entry.data;
    }

    // Verificar L2 (Redis)
    const l2Data = await redis.get(`${this.prefix}:${key}`);
    if (l2Data) {
      const data = JSON.parse(l2Data);
      // Promover para L1
      this.setL1(key, data);
      return data;
    }

    return null;
  }

  async set(key: string, data: T): Promise<void> {
    // Salvar em L1
    this.setL1(key, data);

    // Salvar em L2
    await redis.setex(
      `${this.prefix}:${key}`,
      this.l2Ttl,
      JSON.stringify(data)
    );
  }

  private setL1(key: string, data: T): void {
    // Evicção LRU
    if (this.l1.size >= this.l1MaxSize) {
      const oldestKey = this.l1.keys().next().value;
      this.l1.delete(oldestKey);
    }

    this.l1.set(key, {
      data,
      expiry: Date.now() + this.l1Ttl * 1000,
    });
  }

  async invalidate(key: string): Promise<void> {
    this.l1.delete(key);
    await redis.del(`${this.prefix}:${key}`);
  }
}

Lock Distribuído

Implementação Redlock

// distributed-lock.ts
import Redlock from 'redlock';

const redlock = new Redlock([redis], {
  driftFactor: 0.01,
  retryCount: 10,
  retryDelay: 200,
  retryJitter: 200,
  automaticExtensionThreshold: 500,
});

// Exemplo de uso
async function processOrderWithLock(orderId: string) {
  let lock;

  try {
    // Adquirir lock
    lock = await redlock.acquire([`lock:order:${orderId}`], 30000);

    // Seção crítica
    const order = await db.order.findUnique({ where: { id: orderId } });

    if (order?.status !== 'pending') {
      throw new Error('Order already processed');
    }

    await db.order.update({
      where: { id: orderId },
      data: { status: 'processing' },
    });

    // Executar processamento...

    await db.order.update({
      where: { id: orderId },
      data: { status: 'completed' },
    });
  } catch (error) {
    if (error instanceof Redlock.LockError) {
      console.log('Could not acquire lock, order is being processed');
    }
    throw error;
  } finally {
    // Liberar lock
    if (lock) {
      await lock.release();
    }
  }
}

// Implementação simples de lock
class SimpleLock {
  async acquire(key: string, ttl: number = 30000): Promise<string | null> {
    const token = crypto.randomUUID();
    const result = await redis.set(
      `lock:${key}`,
      token,
      'PX',
      ttl,
      'NX'
    );

    return result === 'OK' ? token : null;
  }

  async release(key: string, token: string): Promise<boolean> {
    const script = `
      if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
      else
        return 0
      end
    `;

    const result = await redis.eval(script, 1, `lock:${key}`, token);
    return result === 1;
  }

  async withLock<T>(
    key: string,
    fn: () => Promise<T>,
    ttl: number = 30000
  ): Promise<T> {
    const token = await this.acquire(key, ttl);

    if (!token) {
      throw new Error(`Could not acquire lock for ${key}`);
    }

    try {
      return await fn();
    } finally {
      await this.release(key, token);
    }
  }
}

Gerenciamento de Sessões

// session.ts
import { randomBytes } from 'crypto';

interface Session {
  userId: string;
  email: string;
  role: string;
  createdAt: number;
  lastAccessedAt: number;
}

class RedisSessionStore {
  private readonly prefix = 'session';
  private readonly ttl = 24 * 60 * 60; // 24 horas

  async create(userId: string, userData: Partial<Session>): Promise<string> {
    const sessionId = randomBytes(32).toString('hex');
    const session: Session = {
      userId,
      email: userData.email || '',
      role: userData.role || 'user',
      createdAt: Date.now(),
      lastAccessedAt: Date.now(),
    };

    const key = `${this.prefix}:${sessionId}`;
    await redis.setex(key, this.ttl, JSON.stringify(session));

    // Gerenciar lista de sessões do usuário
    await redis.sadd(`user_sessions:${userId}`, sessionId);
    await redis.expire(`user_sessions:${userId}`, this.ttl);

    return sessionId;
  }

  async get(sessionId: string): Promise<Session | null> {
    const key = `${this.prefix}:${sessionId}`;
    const data = await redis.get(key);

    if (!data) return null;

    const session: Session = JSON.parse(data);

    // Sliding window: estender TTL no acesso
    session.lastAccessedAt = Date.now();
    await redis.setex(key, this.ttl, JSON.stringify(session));

    return session;
  }

  async destroy(sessionId: string): Promise<void> {
    const session = await this.get(sessionId);
    if (session) {
      await redis.srem(`user_sessions:${session.userId}`, sessionId);
    }
    await redis.del(`${this.prefix}:${sessionId}`);
  }

  async destroyAllUserSessions(userId: string): Promise<void> {
    const sessionIds = await redis.smembers(`user_sessions:${userId}`);

    if (sessionIds.length > 0) {
      const keys = sessionIds.map(id => `${this.prefix}:${id}`);
      await redis.del(...keys);
    }

    await redis.del(`user_sessions:${userId}`);
  }

  async getUserSessionCount(userId: string): Promise<number> {
    return redis.scard(`user_sessions:${userId}`);
  }
}

const sessionStore = new RedisSessionStore();
export { sessionStore };

Rate Limiting

Janela Fixa

// rate-limiter.ts

class FixedWindowRateLimiter {
  constructor(
    private windowSizeSeconds: number,
    private maxRequests: number
  ) {}

  async isAllowed(key: string): Promise<{ allowed: boolean; remaining: number }> {
    const windowKey = `ratelimit:${key}:${Math.floor(Date.now() / 1000 / this.windowSizeSeconds)}`;

    const current = await redis.incr(windowKey);

    if (current === 1) {
      await redis.expire(windowKey, this.windowSizeSeconds);
    }

    const allowed = current <= this.maxRequests;
    const remaining = Math.max(0, this.maxRequests - current);

    return { allowed, remaining };
  }
}

Log de Janela Deslizante

class SlidingWindowLogRateLimiter {
  constructor(
    private windowSizeMs: number,
    private maxRequests: number
  ) {}

  async isAllowed(key: string): Promise<{ allowed: boolean; remaining: number; retryAfter?: number }> {
    const now = Date.now();
    const windowStart = now - this.windowSizeMs;
    const redisKey = `ratelimit:sliding:${key}`;

    // Executar em transação
    const multi = redis.multi();

    // Remover entradas antigas
    multi.zremrangebyscore(redisKey, 0, windowStart);

    // Contar requisições na janela atual
    multi.zcard(redisKey);

    // Obter resultados
    const results = await multi.exec();
    const currentCount = results![1][1] as number;

    if (currentCount >= this.maxRequests) {
      // Obter tempo da requisição mais antiga para calcular retry time
      const oldestRequest = await redis.zrange(redisKey, 0, 0, 'WITHSCORES');
      const retryAfter = oldestRequest.length > 0
        ? Math.ceil((parseInt(oldestRequest[1]) + this.windowSizeMs - now) / 1000)
        : 1;

      return {
        allowed: false,
        remaining: 0,
        retryAfter,
      };
    }

    // Adicionar nova requisição
    await redis.zadd(redisKey, now, `${now}:${Math.random()}`);
    await redis.expire(redisKey, Math.ceil(this.windowSizeMs / 1000));

    return {
      allowed: true,
      remaining: this.maxRequests - currentCount - 1,
    };
  }
}

Token Bucket

class TokenBucketRateLimiter {
  constructor(
    private bucketSize: number,      // Capacidade máxima do bucket
    private refillRate: number,       // Tokens recarregados por segundo
    private refillIntervalMs: number = 1000
  ) {}

  async isAllowed(key: string, tokensRequired: number = 1): Promise<{
    allowed: boolean;
    tokens: number;
    retryAfter?: number;
  }> {
    const redisKey = `ratelimit:bucket:${key}`;

    // Processar atomicamente com script Lua
    const script = `
      local bucket_key = KEYS[1]
      local bucket_size = tonumber(ARGV[1])
      local refill_rate = tonumber(ARGV[2])
      local refill_interval = tonumber(ARGV[3])
      local tokens_required = tonumber(ARGV[4])
      local now = tonumber(ARGV[5])

      local bucket = redis.call('HMGET', bucket_key, 'tokens', 'last_refill')
      local tokens = tonumber(bucket[1]) or bucket_size
      local last_refill = tonumber(bucket[2]) or now

      -- Recarregar tokens
      local time_passed = now - last_refill
      local refills = math.floor(time_passed / refill_interval)
      tokens = math.min(bucket_size, tokens + refills * refill_rate)
      last_refill = last_refill + refills * refill_interval

      if tokens >= tokens_required then
        tokens = tokens - tokens_required
        redis.call('HMSET', bucket_key, 'tokens', tokens, 'last_refill', last_refill)
        redis.call('EXPIRE', bucket_key, 3600)
        return {1, tokens}
      else
        redis.call('HMSET', bucket_key, 'tokens', tokens, 'last_refill', last_refill)
        redis.call('EXPIRE', bucket_key, 3600)
        local tokens_needed = tokens_required - tokens
        local wait_time = math.ceil(tokens_needed / refill_rate * refill_interval)
        return {0, tokens, wait_time}
      end
    `;

    const result = await redis.eval(
      script,
      1,
      redisKey,
      this.bucketSize,
      this.refillRate,
      this.refillIntervalMs,
      tokensRequired,
      Date.now()
    ) as [number, number, number?];

    return {
      allowed: result[0] === 1,
      tokens: result[1],
      retryAfter: result[2] ? Math.ceil(result[2] / 1000) : undefined,
    };
  }
}

Monitoramento e Métricas

// redis-metrics.ts

class RedisMetrics {
  async getInfo(): Promise<Record<string, string>> {
    const info = await redis.info();
    const lines = info.split('\r\n');
    const result: Record<string, string> = {};

    for (const line of lines) {
      if (line.includes(':')) {
        const [key, value] = line.split(':');
        result[key] = value;
      }
    }

    return result;
  }

  async getKeyCount(): Promise<number> {
    const info = await this.getInfo();
    return parseInt(info['db0']?.split(',')[0]?.split('=')[1] || '0');
  }

  async getMemoryUsage(): Promise<{
    used: number;
    peak: number;
    fragmentation: number;
  }> {
    const info = await this.getInfo();
    return {
      used: parseInt(info['used_memory'] || '0'),
      peak: parseInt(info['used_memory_peak'] || '0'),
      fragmentation: parseFloat(info['mem_fragmentation_ratio'] || '1'),
    };
  }

  async getHitRate(): Promise<number> {
    const info = await this.getInfo();
    const hits = parseInt(info['keyspace_hits'] || '0');
    const misses = parseInt(info['keyspace_misses'] || '0');
    const total = hits + misses;

    return total > 0 ? hits / total : 0;
  }

  async getSlowLogs(count: number = 10): Promise<any[]> {
    return redis.slowlog('GET', count);
  }
}

const redisMetrics = new RedisMetrics();
export { redisMetrics };

Resumo

Redis é uma ferramenta poderosa para acesso rápido a dados.

Escolha da Estratégia de Cache

PadrãoCaso de Uso
Cache-AsideMuitas leituras, poucas atualizações
Write-ThroughPrioriza consistência
Write-BehindPrioriza performance de escrita

Boas Práticas

  1. Design de TTL adequado: Configuração baseada nas características dos dados
  2. Design de chaves: Namespacing e versionamento
  3. Pool de conexões: Reutilização de conexões
  4. Monitoramento: Taxa de hit e uso de memória
  5. Failover: Redis Cluster/Sentinel

Com o uso adequado do Redis, você pode melhorar significativamente a performance da sua aplicação.

← Voltar para a lista