TTL e expiração de chaves no Redis

1. Fundamentos do TTL no Redis

TTL (Time-To-Live) é um mecanismo fundamental em bancos de dados em memória como o Redis, permitindo que chaves expirem automaticamente após um período definido. Diferente de bancos SQL relacionais, onde a expiração de dados geralmente requer triggers, eventos agendados (como eventos no MySQL) ou jobs externos (como pg_cron no PostgreSQL), o Redis implementa a expiração diretamente no motor de armazenamento, oferecendo desempenho e simplicidade superiores para casos de uso como cache e sessões.

Os comandos básicos para trabalhar com TTL incluem:

EXPIRE chave segundos        # Define expiração em segundos
PEXPIRE chave milissegundos  # Define expiração em milissegundos
TTL chave                    # Retorna tempo restante em segundos (-1 se não expira, -2 se não existe)
PTTL chave                   # Retorna tempo restante em milissegundos

Exemplo prático:

> SET usuario:123 "João Silva"
OK
> EXPIRE usuario:123 3600
(integer) 1
> TTL usuario:123
(integer) 3598

2. Estratégias de Definição de Expiração

O Redis oferece múltiplas formas de definir expiração, cada uma adequada a diferentes cenários:

Definição no momento da criação:

SETEX chave segundos valor           # Define string com expiração em segundos
PSETEX chave milissegundos valor     # Define string com expiração em ms
SET chave valor EX segundos          # Sintaxe alternativa (Redis 2.6.12+)
SET chave valor PX milissegundos     # Sintaxe alternativa com ms

Exemplo:

> SETEX cache:pagina:home 300 "<html>...</html>"
OK
> TTL cache:pagina:home
(integer) 297

Adição posterior com timestamp Unix:

EXPIREAT chave timestamp_unix        # Expira em data/hora específica
PEXPIREAT chave timestamp_ms         # Expira em timestamp com milissegundos

Remoção de expiração:

PERSIST chave                        # Remove expiração, chave torna-se permanente
> EXPIREAT sessao:abc 1893456000     # Expira em 01/01/2030
(integer) 1
> PERSIST sessao:abc
(integer) 1
> TTL sessao:abc
(integer) -1                        # -1 indica que não expira mais

3. Mecanismos Internos de Expiração

O Redis implementa duas estratégias complementares para expiração de chaves:

Estratégia Passiva (Lazy Expiration): Quando uma chave é acessada (via GET, EXISTS, etc.), o Redis verifica se ela está expirada. Se sim, a chave é removida e o comando retorna como se a chave não existisse.

> SETEX temp:log 10 "dado temporário"
OK
# Após 10 segundos...
> GET temp:log
(nil)                              # Chave foi removida no acesso

Estratégia Ativa (Active Expiration Cycle): A cada 100ms, o Redis executa um algoritmo de amostragem aleatória:
1. Seleciona 20 chaves com expiração definida
2. Remove todas que estão expiradas
3. Se mais de 25% das amostras estavam expiradas, repete o processo

Este mecanismo garante que chaves expiradas não ocupem memória indefinidamente, mas pode haver um curto período onde chaves expiradas ainda existam em memória até serem removidas pelo ciclo ativo.

4. Comportamento com Diferentes Tipos de Dados

A expiração no Redis é sempre aplicada à chave como um todo, não a campos individuais. Isso é crucial para entender o comportamento com tipos complexos:

> HSET sessao:user:456 campo1 "valor1" campo2 "valor2"
(integer) 2
> EXPIRE sessao:user:456 600
(integer) 1
# Após 600 segundos, TODA a hash é removida

Efeito de comandos modificadores: Comandos como LPUSH, HSET, SADD não afetam a expiração já definida:

> EXPIRE lista:tarefas 3600
(integer) 1
> LPUSH lista:tarefas "nova tarefa"    # Expiração continua intacta
(integer) 1
> TTL lista:tarefas
(integer) 3595                         # TTL continua contando

Strings: São o tipo mais comum para expiração, frequentemente usado em cache.

Lists, Sets, Sorted Sets, Hashes: Todos suportam expiração normalmente, mas a expiração sempre remove todo o conjunto de dados.

5. Expiração em Operações Atômicas e Transações

O Redis garante atomicidade mesmo com comandos de expiração dentro de transações:

MULTI
SET chave:transacao "dados importantes"
EXPIRE chave:transacao 300
EXEC

Pipelines e Scripts Lua:

EVAL "redis.call('SET', KEYS[1], ARGV[1]); redis.call('EXPIRE', KEYS[1], ARGV[2])" 1 chave:lua "valor" 600

A expiração é aplicada somente após o commit da transação (EXEC) ou conclusão do script Lua. Em pipelines, a expiração é processada na ordem de execução, mas todas as operações são aplicadas atomicamente.

6. Monitoramento e Debug de Expiração

Para depurar e monitorar expirações, o Redis oferece várias ferramentas:

OBJECT idletime chave              # Tempo desde último acesso (em segundos)
DEBUG OBJECT chave                 # Informações detalhadas da chave

Monitoramento em tempo real:

> MONITOR
# Mostra todas as operações, inclusive expirações automáticas

Métricas do servidor:

> INFO keyspace
# db0:keys=1500,expires=342,avg_ttl=45210

A métrica expired_keys no comando INFO stats mostra o total de chaves expiradas desde o início do servidor. Ferramentas como RedisInsight e exporters para Prometheus permitem visualizar essas métricas em dashboards.

7. Casos de Uso Práticos e Padrões

Cache com expiração automática:

> SET cache:consulta:relatorio "dados processados" EX 1800
# Expira em 30 minutos, similar a tabelas temporárias em SQL

Sessões de usuário com renovação por inatividade:

# A cada requisição do usuário:
> GET sessao:user:789
"dados da sessao"
> EXPIRE sessao:user:789 1800    # Renova por mais 30 minutos

Rate limiting com janela de tempo:

# Contador de requisições por IP em 1 minuto
> INCR rate:ip:192.168.1.1
(integer) 1
> EXPIRE rate:ip:192.168.1.1 60
(integer) 1
# Se o contador exceder o limite, bloqueia requisições

Locks distribuídos com tempo máximo de vida:

> SET lock:recurso:processo "owner:servidor1" EX 10 NX
# Lock expira automaticamente em 10s para evitar deadlocks

8. Armadilhas e Boas Práticas

Chaves sem expiração (TTL=-1): Podem causar vazamento de memória. Sempre defina TTL para dados temporários.

Renovação excessiva de TTL em alta concorrência:

# Problema: race condition entre GET e EXPIRE
> GET chave:concorrente
> EXPIRE chave:concorrente 60   # Pode perder dados entre comandos

Solução atômica com SET ... GET (Redis 6.2+):

> SET chave:concorrente "valor" EX 60 GET
# Renova atômica e retorna valor anterior

Cuidados com EXPIREAT e timezones: Use timestamps Unix (UTC) para evitar problemas com fusos horários:

# Correto:
> EXPIREAT chave:global 1893456000

# Incorreto (depende do fuso do servidor):
> EXPIREAT chave:local 1893456000

Sincronização de relógios: Em clusters Redis, a expiração baseada em EXPIREAT depende de relógios sincronizados (NTP). Diferenças de horário entre nós podem causar comportamentos inesperados.

Boas práticas finais:
- Sempre verifique o TTL após definir expiração
- Use PERSIST com cautela em dados que deveriam expirar
- Para expiração em massa, considere o comando UNLINK (assíncrono) ao invés de DEL
- Monitore expired_keys para ajustar parâmetros de expiração

Referências