Como implementar expiração automática de dados com TTL no Redis e PostgreSQL
1. Fundamentos do TTL e expiração automática de dados
Time-To-Live (TTL) é um mecanismo que define automaticamente o tempo de vida de um dado, removendo-o quando esse período expira. Em sistemas modernos, o TTL é essencial para gerenciar dados temporários como sessões de usuário, tokens de autenticação, caches de API, logs transitórios e códigos de verificação.
A diferença fundamental entre Redis e PostgreSQL está na abordagem: o Redis foi projetado nativamente para expiração, com remoção automática integrada ao seu ciclo de vida em memória. Já o PostgreSQL, sendo um banco relacional persistente, exige mecanismos adicionais para simular esse comportamento — como colunas de timestamp combinadas com limpeza programada.
2. Implementando TTL no Redis: comandos e estratégias
O Redis oferece comandos diretos para TTL. O exemplo abaixo mostra operações básicas:
# Definir chave com expiração de 60 segundos
SET user:session:123 "dados-da-sessao"
EXPIRE user:session:123 60
# Verificar tempo restante (em segundos)
TTL user:session:123
# Definir e expirar em um único comando
SETEX user:token:456 "token-valido" 3600
# Expiração em milissegundos
PEXPIRE user:precise:789 5000
# Remover expiração
PERSIST user:session:123
Para estruturas complexas como hashes, o TTL deve ser aplicado à chave principal:
# Hash com expiração
HSET user:profile:999 nome "João" email "joao@email.com"
EXPIRE user:profile:999 7200
O Redis implementa duas estratégias de remoção: lazy expiration (remove ao acessar uma chave expirada) e active expiration (processo em background que varre chaves expiradas a cada 100ms). Para conjuntos ordenados com expiração, use ZREMRANGEBYSCORE combinado com timestamps.
3. Expiração automática no PostgreSQL: tabelas temporárias e eventos
No PostgreSQL, usamos colunas de timestamp para controle de expiração:
-- Tabela com controle de expiração
CREATE TABLE sessoes (
id SERIAL PRIMARY KEY,
usuario_id INTEGER NOT NULL,
token VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP DEFAULT NOW() + INTERVAL '1 hour'
);
-- Função de limpeza automática
CREATE OR REPLACE FUNCTION limpar_sessoes_expiradas()
RETURNS VOID AS $$
BEGIN
DELETE FROM sessoes WHERE expires_at < NOW();
END;
$$ LANGUAGE plpgsql;
-- Trigger para remoção automática antes de inserção
CREATE OR REPLACE FUNCTION trigger_limpeza_ao_inserir()
RETURNS TRIGGER AS $$
BEGIN
DELETE FROM sessoes WHERE expires_at < NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trigger_limpeza
BEFORE INSERT ON sessoes
FOR EACH ROW
EXECUTE FUNCTION trigger_limpeza_ao_inserir();
Para agendamento periódico com pg_cron:
-- Instalar extensão (requer superusuário)
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- Agendar limpeza a cada 5 minutos
SELECT cron.schedule('limpeza-sessoes', '*/5 * * * *',
$$DELETE FROM sessoes WHERE expires_at < NOW()$$);
4. Comparação de desempenho e custos entre Redis e PostgreSQL
O Redis oferece latência sub-milissegundo para operações de TTL, ideal para cache de alta frequência. O PostgreSQL, embora mais lento (2-10ms), garante persistência em disco e transações ACID.
Em termos de custos, o Redis consome RAM — quanto mais chaves com TTL, maior o uso de memória. O PostgreSQL usa espaço em disco, mais barato que RAM, mas com overhead de I/O para operações de limpeza.
O trade-off principal: Redis prioriza velocidade e simplicidade; PostgreSQL prioriza durabilidade e consistência. Para dados críticos que não podem ser perdidos (como tokens de pagamento), PostgreSQL é recomendado. Para caches voláteis (como resultados de consultas frequentes), Redis é superior.
5. Padrões híbridos: combinando Redis e PostgreSQL para expiração
Um padrão robusto usa Redis como cache com TTL curto e PostgreSQL como armazenamento persistente:
# Fluxo híbrido:
# 1. Verificar Redis primeiro
# 2. Se não existir, buscar no PostgreSQL
# 3. Armazenar no Redis com TTL
# 4. PostgreSQL mantém registro permanente
# Exemplo em pseudocódigo:
# if redis.exists("user:123"):
# return redis.get("user:123")
# else:
# data = postgres.query("SELECT * FROM usuarios WHERE id=123")
# redis.setex("user:123", 300, data)
# return data
Para sincronização de expiração, use um campo expires_at comum em ambos os sistemas. A consistência eventual é aceitável para a maioria dos casos — o Redis pode ter dados ligeiramente desatualizados, mas o PostgreSQL mantém a versão verdadeira.
6. Boas práticas e armadilhas comuns na implementação de TTL
Armadilhas comuns:
- Não definir TTL em chaves de cache, causando acúmulo infinito de memória
- Definir TTL muito curto para dados que demoram a ser recalculados
- Ignorar a renovação de TTL em sessões ativas (use EXPIRE novamente a cada requisição)
- Realizar expiração em massa sem planejamento, causando picos de carga
Boas práticas:
- Monitore chaves expiradas com INFO keyspace no Redis
- Use SCAN em vez de KEYS para evitar bloqueios
- No PostgreSQL, crie índices em expires_at para acelerar as consultas de limpeza
- Implemente logs de expiração para auditoria de dados removidos
7. Exemplos práticos de código e configuração
Pipeline completo no Redis:
# Configuração de sessão com renovação automática
MULTI
SETEX session:user:100 "token-abc-123" 3600
EXPIRE session:user:100 3600
EXEC
# Verificar e renovar
TTL session:user:100
# Se TTL < 300 segundos, renovar
EXPIRE session:user:100 3600
# Remover chave expirada manualmente (lazy)
GET session:user:100
# Retorna nil se expirado
Função SQL completa para PostgreSQL com pg_cron:
-- Tabela de tokens de API
CREATE TABLE api_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
usuario_id INTEGER NOT NULL,
token_hash VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL
);
-- Índice para acelerar limpeza
CREATE INDEX idx_expires_at ON api_tokens(expires_at);
-- Função de limpeza com log
CREATE OR REPLACE FUNCTION limpar_tokens_expirados()
RETURNS INTEGER AS $$
DECLARE
removidos INTEGER;
BEGIN
DELETE FROM api_tokens WHERE expires_at < NOW();
GET DIAGNOSTICS removidos = ROW_COUNT;
INSERT INTO logs_limpeza (tabela, registros_removidos, data)
VALUES ('api_tokens', removidos, NOW());
RETURN removidos;
END;
$$ LANGUAGE plpgsql;
-- Agendamento a cada minuto
SELECT cron.schedule('limpeza-tokens', '* * * * *',
'SELECT limpar_tokens_expirados();');
Aplicação real combinando ambos:
# Fluxo completo de autenticação
1. Usuário faz login
2. PostgreSQL armazena sessão permanente (expira em 24h)
3. Redis armazena cache da sessão (expira em 1h)
4. A cada requisição, verifica Redis primeiro
5. Se Redis expirou, busca no PostgreSQL e recria cache
6. Se PostgreSQL expirou, força novo login
Referências
- Redis Documentation: EXPIRE command — Documentação oficial do comando EXPIRE com exemplos e detalhes de implementação
- PostgreSQL Documentation: Automatic Deletion with pg_cron — Guia oficial sobre agendamento de tarefas no PostgreSQL
- Redis: Key Expiration and Eviction Policies — Explicação detalhada das políticas de expiração e remoção de chaves no Redis
- PostgreSQL: Triggers and Automatic Cleanup Strategies — Tutorial completo sobre triggers no PostgreSQL para automação de limpeza
- Combining Redis and PostgreSQL for High-Performance Applications — Artigo técnico sobre arquiteturas híbridas com Redis e PostgreSQL para expiração de dados