Rate Limiting Distribuído com Redis e Lua Scripts: Controle de Taxa Atômico
Aprenda a implementar controle de taxa distribuído em microsserviços usando Redis e scripts Lua. Garanta operações atômicas, elimine concorrência destrutiva e proteja APIs contra sobrecargas com exemplos práticos de código.
Resumo
- Operações atômicas no Redis evitam que requisições simultâneas corrompam contadores de acesso em ambientes distribuídos.
- Scripts Lua executam diretamente no servidor de banco de dados, garantindo que leitura, lógica e escrita ocorram sem interrupções.
- A abordagem baseada em janela deslizante oferece precisão superior frente ao modelo clássico de janela fixa.
- Servidores modernos de cache em memória eliminam gargalos de rede ao consolidar a tomada de decisão em uma única chamada.
- Sistemas de alta escala exigem estratégias combinadas de bloqueio e degradação elegante para manter a estabilidade sob picos.
O Desafio do Controle de Taxa em Arquiteturas Distribuídas
Quando desenvolvemos aplicações modernas voltadas para a nuvem, é comum dividir o sistema em vários microsserviços independentes. Essa divisão melhora a flexibilidade, mas traz um problema clássico: como impedir que um único usuário ou um script malicioso sobrecarregue nossas portas de entrada? O controle de taxa, conhecido na engenharia de software como rate limiting, atua justamente como um segurança na porta da balada, limitando quantas vezes um cliente pode chamar determinada rota em um intervalo específico de tempo.
Em sistemas monolíticos tradicionais, guardar essa contagem na memória RAM do próprio servidor resolve o problema de forma simples. No entanto, quando escalamos nossa infraestrutura para rodar em dezenas de servidores paralelos — balanceados por um roteador de tráfego —, o cenário muda drasticamente. Se o Servidor A contabiliza três requisições de um usuário e o Servidor B contabiliza outras quatro para o mesmo cliente, o limite global pode ser violado facilmente, pois os servidores não conversam entre si em tempo real sobre cada clique individual.
Para centralizar essa contagem, adotamos um banco de dados em memória compartilhado, sendo o Redis a escolha mais comum na indústria devido à sua velocidade extrema. Mas colocar o Redis no meio do caminho não resolve tudo magicamente. Se dois servidores tentarem atualizar o limite do mesmo usuário no mesmo milissegundo, corremos o risco de enfrentar o que chamamos de condição de corrida, onde atualizações simultâneas se atropelam e geram dados imprecisos, permitindo acessos indevidos.
Entendendo o Problema da Concorrência Destrutiva
Imagine que dois amigos decidem somar dinheiro em um cofrinho ao mesmo tempo, sem olhar um para o que o outro está colocando. O primeiro pega o valor atual que é dez reais, soma mais cinco e se prepara para fechar a tampa. Exatamente no mesmo instante, o segundo pega o mesmo valor inicial de dez reais, soma mais dez e fecha a tampa. O resultado final no cofrinho será vinte reais, quando na verdade deveria ser vinte e cinco. O depósito do primeiro amigo foi apagado pelo segundo.
No mundo dos bancos de dados, esse fenômeno é conhecido como leitura-modificação-escrita sem proteção atômica. Quando um microsserviço precisa verificar se o usuário excedeu o limite de requisições, ele normalmente executa três passos distintos: lê o valor atual do contador no Redis, soma mais um no código da aplicação e escreve o novo valor de volta no servidor de cache. Se centenas de requisições chegarem simultaneamente para o mesmo usuário, esses passos se sobrepõem e centenas de acessos contam como apenas um.
A consequência direta dessa falha é a quebra completa da política de segurança e proteção da API. Um usuário mal-intencionado, enviando requisições em rajadas perfeitamente sincronizadas, consegue burlar os limites estipulados, sobrecarregando o banco de dados principal ou esgotando recursos caros de processamento. Precisamos, portanto, de um mecanismo que garanta que essas três etapas aconteçam como uma transação indivisível, onde nada mais pode interferir no meio do caminho.
A Solução com Scripts Lua no Redis
Para eliminar o problema da concorrência destrutiva sem precisar travar tabelas inteiras ou criar mecanismos complexos de locks distribuídos, podemos recorrer aos scripts Lua integrados ao Redis. A linguagem Lua é leve, rápida e amplamente utilizada para estender funcionalidades em softwares de alta performance. Na prática, o Redis permite que você envie um bloco de código escrito em Lua para ser executado diretamente dentro do motor do banco de dados.
A grande vantagem dessa abordagem é a atomicidade nativa. O Redis garante que, enquanto um script Lua estiver rodando, nenhuma outra operação ou comando enviado por outros clientes será executado. É como se o banco de dados parasse o relógio por alguns milissegundos para rodar sua lógica inteira — leitura, validação e gravação — em um único sopro ininterrupto. Nenhum outro servidor consegue se intrometer no meio do processo.
Além de garantir a integridade dos dados, usar scripts Lua reduz drasticamente o tráfego de rede entre a aplicação e o servidor de cache. Em vez de fazer várias idas e vindas perguntando e atualizando valores, a aplicação envia um único pacote de texto contendo o script e os parâmetros necessários, recebe o resultado final de forma imediata e decide se a requisição deve ser aceita ou rejeitada.
Implementação Prática do Algoritmo de Janela Deslizante
Existem várias estratégias de controle de taxa, como a janela fixa (que zera a contagem a cada virada de hora ou minuto) e o balde de gotejamento. Uma das abordagens mais robustas para ambientes de alta precisão é a janela deslizante baseada em registros de tempo armazenados em estruturas de conjuntos ordenados do Redis, conhecidas como ZSETs.
Abaixo apresentamos um exemplo de script Lua que implementa essa lógica de forma concisa e totalmente atômica. Ele remove registros antigos que já passaram da janela de tempo, conta quantos acessos restam no período atual e adiciona a nova requisição caso o limite ainda não tenha sido atingido.
local key = KEYS[1]local now = tonumber(ARGV[1])local window = tonumber(ARGV[2])local limit = tonumber(ARGV[3])local clear_before = now - window-- Remove registros antigos fora da janela de temporedisch.call('ZREMRANGEBYSCORE', key, 0, clear_before)-- Conta quantos acessos ainda existem na janelalocal current_requests = redis.call('ZCARD', key)if current_requests < limit then -- Adiciona a requisição atual com o timestamp como score redis.call('ZADD', key, now, now) -- Define um tempo de expiração para limpar a chave sozinha redis.call('EXPIRE', key, math.ceil(window / 1000)) return 1else return 0endNo código acima, KEYS[1] representa a chave única do usuário ou IP que está sendo monitorado. O argumento now traz o momento exato da requisição em milissegundos, enquanto window define o tamanho da janela de análise (por exemplo, sessenta segundos) e limit estabelece o número máximo de chamadas permitidas. Se o script retornar o número um, a requisição é aprovada; se retornar zero, o sistema retorna um erro informando que o limite foi excedido.
Integrando o Script Lua na Aplicação Backend
Escrever o script Lua é apenas a metade do caminho; precisamos consumi-lo de maneira eficiente a partir do nosso microsserviço backend, seja ele construído em Node.js, Python, Go ou Java. Para evitar o trabalho desnecessário de enviar todo o texto do script pela rede a cada requisição realizada pelos clientes, os clientes modernos do Redis utilizam um recurso chamado de carregamento prévio por hash.
O cliente calcula uma assinatura digital única — conhecida como SHA1 — do script Lua na inicialização da aplicação e pede para o Redis armazená-lo em cache interno. Nas requisições do dia a dia, a aplicação apenas envia o código SHA1 e os parâmetros correspondentes. Isso economiza banda de rede e acelera o tempo de resposta, mantendo o fluxo de atendimento extremamente ágil mesmo sob tráfego intenso.
Caso o Redis seja reiniciado e perca os scripts em cache, os clientes inteligentes possuem mecanismos automáticos de recuperação que identificam o erro de script não encontrado e reenviam o código fonte completo instantaneamente. Essa resiliência operacional é fundamental para garantir que uma manutenção na infraestrutura de cache não derrube o sistema inteiro por falta de dependências.
Considerações Operacionais e Monitoramento
Embora os scripts Lua no Redis resolvam o problema da concorrência com elegância, precisamos ter cautela com o tempo de execução do código enviado. Como o Redis opera em uma única linha principal de execução para processar comandos — conhecida como single-threaded event loop —, qualquer script Lua que demore muito tempo para rodar vai congelar o servidor inteiro, bloqueando todas as outras aplicações que dependem dele.
Portanto, mantenha seus scripts curtos, focados apenas na lógica matemática estritamente necessária. Evite laços de repetição complexos, manipulação excessiva de strings grandes ou operações custosas dentro do script. Monitore constantemente métricas como o tempo de resposta do Redis e o uso de CPU para identificar gargalos antes que eles afetem os usuários finais.
Outro ponto importante diz respeito ao planejamento da capacidade do cluster de Redis. Como cada chave de controle de taxa consome memória para armazenar os registros de tempo, garanta que suas chaves possuam políticas de expiração adequadas configuradas através do comando EXPIRE. Assim, usuários inativos deixam de ocupar espaço precioso na memória RAM do servidor automaticamente.
Conclusão e Próximos Passos
O controle de taxa distribuído deixa de ser um enigma complexo quando combinamos a velocidade do Redis com a atomicidade proporcionada pelos scripts Lua. Essa arquitetura elimina os riscos de condições de corrida e protege nossas APIs contra tráfego abusivo sem sacrificar a performance e a escalabilidade horizontal dos microsserviços.
Ao implementar essa solução em seus projetos, comece testando o comportamento sob carga simulada com ferramentas de teste de estresse. Valide se os limites configurados respondem corretamente sob rajadas e ajuste os tempos de expiração conforme o perfil de uso dos seus clientes para construir uma aplicação resiliente e preparada para crescer.