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