Limitação de Taxa Utilizando Redis

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.

  1. 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.

  1. 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.

  1. 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.

Publicado em 7-3 21:52