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
retryStrategypara tentar reconectar após falhas - Timeouts: defina
connectTimeout(10000ms) ecommandTimeout(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
- Documentação Oficial do Redis - Sessões — Guia oficial sobre armazenamento de sessões com Redis, incluindo exemplos práticos e boas práticas
- ioredis - Cliente Redis para Node.js — Biblioteca robusta para conexão com Redis, com suporte a cluster, sentinel e pooling
- Redis Session Store - Express.js — Middleware oficial para gerenciamento de sessões em aplicações Express com Redis
- Redis Cluster Tutorial — Guia completo para configurar Redis Cluster em produção com alta disponibilidade
- Redis Keyspace Notifications — Documentação sobre notificações de eventos de chave, útil para monitorar expiração de sessões
- Redis Memory Eviction Policies — Explicação detalhada das políticas de evicção de memória e como configurá-las
- OpenTelemetry Redis Instrumentation — Guia para adicionar tracing e monitoramento em operações Redis