Como usar o Redis para cache de sessões em aplicações stateless

1. Fundamentos: Sessões em Arquiteturas Stateless

1.1. O problema da persistência de estado em aplicações horizontais

Em aplicações web modernas, a escalabilidade horizontal é essencial. Quando múltiplas instâncias de uma aplicação são executadas atrás de um balanceador de carga, cada requisição pode cair em uma instância diferente. Se o estado da sessão for armazenado localmente na memória de uma instância específica, as requisições subsequentes perderão o contexto do usuário, resultando em falhas de autenticação e perda de dados temporários.

1.2. Diferença entre sessões stateful vs. stateless

Em uma arquitetura stateful, a sessão é mantida na memória da própria aplicação. Exemplo típico: sessões armazenadas em dicionários ou arrays em memória local. Isso funciona bem para uma única instância, mas quebra completamente em cenários com múltiplas réplicas.

Em uma arquitetura stateless, a aplicação não armazena estado localmente. Em vez disso, utiliza um cache externo compartilhado, como o Redis. Cada instância pode ler e escrever dados de sessão no mesmo repositório centralizado, garantindo consistência independentemente de qual instância atende a requisição.

1.3. Por que Redis é a escolha ideal

O Redis oferece três características fundamentais para cache de sessões:

  • Velocidade: opera em memória RAM, com latências tipicamente abaixo de 1ms
  • TTL (Time-To-Live): expiração automática de chaves, ideal para sessões com tempo limitado
  • Estruturas de dados flexíveis: hashes permitem armazenar múltiplos campos da sessão em uma única chave

2. Configuração e Conexão com Redis para Sessões

2.1. Instalação e configuração básica do Redis

Para ambientes de desenvolvimento, o Redis standalone é suficiente. Em produção, considere Redis Cluster ou Sentinel.

# Instalação via Docker
docker run --name redis-session -p 6379:6379 -d redis:7-alpine

# Verificar se está rodando
docker ps | grep redis-session

2.2. Configuração de cliente Redis na aplicação

Exemplo com Node.js e ioredis:

const Redis = require('ioredis');

const redisClient = new Redis({
  host: 'localhost',
  port: 6379,
  password: process.env.REDIS_PASSWORD || '',
  retryStrategy: (times) => Math.min(times * 50, 2000),
  maxRetriesPerRequest: 3,
  enableReadyCheck: true,
});

redisClient.on('error', (err) => console.error('Redis error:', err));
redisClient.on('connect', () => console.log('Conectado ao Redis'));

2.3. Estratégias de conexão

  • Pooling: reutilize conexões em vez de criar novas a cada requisição
  • Reconexão automática: configure retryStrategy para tentar reconectar após falhas
  • Timeouts: defina connectTimeout (10000ms) e commandTimeout (5000ms)

3. Estrutura de Dados para Armazenamento de Sessões

3.1. Uso de hashes Redis

Hashes são ideais para sessões porque permitem armazenar e recuperar campos individuais sem transferir todo o objeto:

// Salvar sessão
await redisClient.hset('session:abc123', {
  userId: 42,
  username: 'joao',
  role: 'admin',
  lastActivity: Date.now(),
  ip: '192.168.1.1'
});

// Recuperar campo específico
const userId = await redisClient.hget('session:abc123', 'userId');

// Recuperar toda a sessão
const session = await redisClient.hgetall('session:abc123');

3.2. Chaveamento eficiente

Use prefixos e namespaces para organizar as chaves:

// Formato: session:{id-da-sessao}
// Exemplo: session:8f3a1b2c

// Para múltiplos namespaces:
// app:user:42:session:abc123
// app:admin:session:def456

3.3. Serialização de sessões

Para dados complexos, serialize antes de armazenar:

// JSON (recomendado para interoperabilidade)
const sessionData = JSON.stringify({ userId: 42, permissions: ['read', 'write'] });
await redisClient.set('session:abc123', sessionData);

// Recuperar e desserializar
const raw = await redisClient.get('session:abc123');
const session = JSON.parse(raw);

4. Implementação do Ciclo de Vida da Sessão

4.1. Criação e inicialização no login

const crypto = require('crypto');

async function createSession(user) {
  const sessionId = crypto.randomBytes(32).toString('hex');
  const sessionKey = `session:${sessionId}`;

  await redisClient.hset(sessionKey, {
    userId: user.id,
    username: user.username,
    createdAt: Date.now(),
    lastActivity: Date.now(),
    role: user.role
  });

  // TTL de 30 minutos (1800 segundos)
  await redisClient.expire(sessionKey, 1800);

  return sessionId;
}

4.2. Atualização e extensão de TTL

A cada requisição ativa, renove o TTL da sessão:

async function touchSession(sessionId) {
  const sessionKey = `session:${sessionId}`;

  // Atualizar último acesso
  await redisClient.hset(sessionKey, 'lastActivity', Date.now());

  // Renovar TTL (expiração deslizante)
  await redisClient.expire(sessionKey, 1800);
}

4.3. Invalidação e remoção

// Logout
async function destroySession(sessionId) {
  await redisClient.del(`session:${sessionId}`);
}

// Revogação manual (exemplo: admin removendo usuário)
async function revokeUserSessions(userId) {
  const cursor = '0';
  do {
    const [nextCursor, keys] = await redisClient.scan(
      cursor, 'MATCH', 'session:*', 'COUNT', 100
    );

    for (const key of keys) {
      const userIdInSession = await redisClient.hget(key, 'userId');
      if (userIdInSession === userId.toString()) {
        await redisClient.del(key);
      }
    }
    cursor = nextCursor;
  } while (cursor !== '0');
}

5. Segurança e Boas Práticas no Cache de Sessões

5.1. Criptografia de dados sensíveis

Nunca armazene senhas ou tokens em texto puro no Redis:

const crypto = require('crypto');

function encryptSensitiveData(data, secretKey) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv('aes-256-gcm', secretKey, iv);
  let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
  encrypted += cipher.final('hex');
  return { iv: iv.toString('hex'), encryptedData: encrypted, tag: cipher.getAuthTag().toString('hex') };
}

5.2. Proteção contra ataques

  • Session fixation: gere novo ID de sessão após login
  • Session hijacking: valide IP e User-Agent nas requisições
  • Replay attacks: inclua timestamps e nonces nas operações críticas

5.3. Sessão híbrida com JWT

Combine JWT (para autenticação rápida) com Redis (para dados de sessão detalhados):

// JWT contém apenas sessionId e assinatura
const token = jwt.sign({ sessionId: 'abc123' }, process.env.JWT_SECRET, { expiresIn: '15m' });

// Redis armazena dados completos da sessão
await redisClient.hset('session:abc123', {
  userId: 42,
  permissions: ['read', 'write', 'delete'],
  preferences: { theme: 'dark', language: 'pt-BR' }
});

6. Estratégias de Expiração e Limpeza Automática

6.1. TTL por sessão: expiração deslizante vs. fixa

  • Expiração fixa: TTL definido na criação, não renovado
  • Expiração deslizante: TTL renovado a cada requisição ativa (recomendado)
// Expiração deslizante
await redisClient.expire(sessionKey, 1800);

// Expiração fixa (usando SET com EX)
await redisClient.set(sessionKey, data, 'EX', 1800);

6.2. Políticas de evicção de memória

Configure no redis.conf:

# Remover chaves com TTL próximo de expirar primeiro
maxmemory-policy volatile-ttl

# Ou remover as menos usadas recentemente
maxmemory-policy allkeys-lru

6.3. Monitoramento com Keyspace Notifications

Ative notificações para expiração de chaves:

# No redis.conf
notify-keyspace-events Ex

# Na aplicação
const subscriber = new Redis();
subscriber.psubscribe('__keyevent@0__:expired');
subscriber.on('pmessage', (pattern, channel, key) => {
  if (key.startsWith('session:')) {
    console.log(`Sessão expirada: ${key}`);
    // Executar limpeza adicional se necessário
  }
});

7. Escalabilidade e Alta Disponibilidade

7.1. Redis Sentinel

Para failover automático:

const Redis = require('ioredis');

const redisClient = new Redis({
  sentinels: [
    { host: 'sentinel1.example.com', port: 26379 },
    { host: 'sentinel2.example.com', port: 26379 },
    { host: 'sentinel3.example.com', port: 26379 }
  ],
  name: 'mymaster',
  role: 'master'
});

7.2. Redis Cluster

Para distribuição horizontal:

const Redis = require('ioredis');

const redisClient = new Redis.Cluster([
  { host: 'node1.example.com', port: 6379 },
  { host: 'node2.example.com', port: 6379 },
  { host: 'node3.example.com', port: 6379 }
], {
  redisOptions: {
    password: process.env.REDIS_PASSWORD
  }
});

7.3. Persistência e consistência

  • RDB: snapshots periódicos (bom para recuperação rápida)
  • AOF: log de operações (melhor durabilidade)
  • Consistência eventual: aceitável para sessões, desde que o TTL seja curto

8. Monitoramento e Depuração de Sessões em Produção

8.1. Comandos Redis essenciais

# Ver TTL de uma sessão específica
TTL session:abc123

# Estimar memória usada por uma sessão
MEMORY USAGE session:abc123

# Escanear sessões ativas
SCAN 0 MATCH session:* COUNT 1000

8.2. Métricas-chave

Implemente monitoramento no código:

async function getSessionMetrics() {
  const info = await redisClient.info();
  const keyspace = info.match(/db0:keys=(\d+)/);
  const usedMemory = info.match(/used_memory_human:([^\r\n]+)/);

  return {
    activeSessions: keyspace ? parseInt(keyspace[1]) : 0,
    memoryUsage: usedMemory ? usedMemory[1] : 'unknown',
    hitRatio: await calculateHitRatio()
  };
}

8.3. Logging e tracing

Use Redis Slow Log e OpenTelemetry:

# Configurar slow log (comandos > 10ms)
CONFIG SET slowlog-log-slower-than 10000

# Ver comandos lentos
SLOWLOG GET 10

Referências