Introdução: A implementação de rate limiting é fundamental em diversos cenários de aplicações corporativas. Quando a capacidade de processamento de um sistema é limitada, torna-se necessário controlar quantas vezes um usuário pode executar determinada ação em um período específico. Isso é especialmente importante para evitar abusos, como requisições maliciosas de concorrentes ou sobrecarga dos serviços.
Abordaremos três estratégias principais para implementação de rate limiting utilizando Redis.
- Limitação por Janela Fixa
Um caso prático comum é o envio de códigos de verificação por SMS. Quando um usuário pode solicitar códigos livreemnte, isso gera custos significativos com serviços de terceiros e sobrecarrega o servidor. Portanto, é essencial implementar controles para evitar abusos.
Inicialmente, poderíamos gerar e enviar o código diretamente. Posteriormente, seria necessário implementar um contador para registrar a quantidade de envios. Como essa informação precisa ser compartilhada entre múltiplas instâncias do serviço, o Redis oferece uma solução eficiente através de chaves com tempo de expiração.
A implementação utiliza um contador que incrementa a cada requisição, com expiração de uma hora, limitando a cinco solicitações por período:
public String enviarCodigoVerificacao(String identificadorUsuario, String telefone) throws Exception {
if (!verificarLimiteEnvio(identificadorUsuario)) {
throw new Exception("Muitas solicitações. Aguarde um momento.");
}
return GeradorCodigo.gerarNumerico(6);
}
private boolean verificarLimiteEnvio(String identificadorUsuario) {
String chaveRedis = String.format("sms:envio:%s", identificadorUsuario);
long quantidade = redisTemplate.opsForValue().increment(chaveRedis, 1);
redisTemplate.expire(chaveRedis, 1, TimeUnit.HOURS);
return quantidade <= 5;
}
Ao executar o teste com dez tentativas consecutivas, observa-se que apenas as primeiras cinco requisições são processadas com sucesso. As demais requisições dentro do mesmo período de uma hora são bloqueadas, garantindo assim a proteção contra abusos no serviço de envio de códigos.
Esta abordagem representa a forma mais simples de limitação, utilizando um contador dentro de um intervalo de tempo fixo.
- Limitação por Janela Deslizante
A estratégia anterior apresenta limitações quando aplicada a um endpoint específico com múltiplos usuários. Imagine um cenário onde,我们需要 limitar para 10 requisições a cada 60 segundos. A abordagem de janela fixa apresenta problemas nas bordas do intervalo de tempo, especialmente quando há muchas requisições próximas ao momento de reset da janela.
Para solucionar esse problema, utiliza-se o algoritmo de janela deslizante com Redis. A estrutura Sorted Set (ZSET) do Redis é ideal para essa implementação, pois permite armazenar elementos com scores que representam timestamps, facilitando a remoção de registros antigos que estão fora da janela de tempo.
Exemplo de implementação para limitar 30 requisições por segundo:
/**
* Limitação por janela deslizante
* @param periodoJanela Tempo da janela em segundos
* @param maximoRequisicoes Número máximo de requisições permitidas
*/
public boolean aplicarLimitacaoJanelaDeslizante(int periodoJanela, int maximoRequisicoes) {
String chaveControle = "endpoint:controle:requisicoes";
long momentoAtual = System.currentTimeMillis();
// Adicionar requisição atual com timestamp como score
redisTemplate.opsForZSet().add(chaveControle, String.valueOf(momentoAtual), momentoAtual);
// Remover registros fora da janela de tempo
redisTemplate.opsForZSet().removeRangeByScore(chaveControle, 0, momentoAtual - (periodoJanela * 1000L));
// Contar requisições na janela atual
Long totalRequisicoes = redisTemplate.opsForZSet().size(chaveControle);
// Expirar chave para liberar memória
redisTemplate.expire(chaveControle, periodoJanela + 1, TimeUnit.SECONDS);
return totalRequisicoes <= maximoRequisicoes;
}
A cada requisição, o sistema adiciona um registro com o timestamp atual, remove registros antigos que estão fora da janela, e verifica se o número total de registros está dentro do limite permitido. Utilizando um pool de threads para simular acessos concorrentes, é possível validar que apenas as primeiras 30 requisições dentro de um segundo são processadas, enquanto as demais são bloqueadas.
Podem ser realizadas otimizações adicionais, como o uso de pipelines do Redis para reduzir o número de conexões, e ajuste na granularidade dos timestamps para maior precisão no controle.
Esta estratégia é indicada para cenários onde a janela de tempo é relativamente pequena. Para janelas extensas com grandes volumes de requisições, o consumo de memória pode se tornar elevado, sendo necessário considerar outras abordagens.
- Limitação por Funil (Leaky Bucket)
O algoritmo de limitação por funil, também conhecido como leaky bucket, funciona de maneira análoga a um funil com vazamento. As requisições são representadas como água que entra no funil, e o processamento representa a vazão de saída. O funil possui capacidade limitada, e quando está cheio, novas requisições são rejeitadas até que o processamento libere espaço.
Se a taxa de processamento exceder a taxa de chegada de requisições, o funil nunca atinge sua capacidade máxima. Caso contrário, quando a vazão é menor que a entrada, o funil enche progressivamente até rejeitar novas requisições.
Esta abordagem proporciona um fluxo de processamento mais uniforme, evitando picos abruptos de carga no sistema.
A implementação pode ser realizada utilizando contadores no Redis ou estruturas de dados específicas, dependendo dos requisitos de precisão e escalabilidade da aplicação.