Как реализовать rate limiting с использованием Redis
Что проверяют
Понимаете ли вы, что счётчик в Redis должен быть атомарным и общим для всех экземпляров Node.js, и знаете ли разницу между алгоритмами. Следом обычно спрашивают, что будет с лимитером, если Redis недоступен.
Как отвечать
Начните с выбора алгоритма, потом покажите, как сделать его атомарным:
- фиксированное окно:
INCRпо ключу видаrl:{userId}:{минута}, и только если вернулась единица,EXPIREна длину окна. Дёшево, но на стыке окон клиент успевает сделать почти двойной лимит; - скользящий журнал: sorted set,
ZADDс меткой времени,ZREMRANGEBYSCOREдля старых записей иZCARD. Точно, но память растёт с числом запросов; - скользящий счётчик: два соседних фиксированных окна, предыдущее учитывается с весом. Компромисс по точности и памяти;
- token bucket: в ключе хранятся остаток токенов и время последнего пополнения, короткие всплески разрешены.
Любая схема из нескольких команд оформляется Lua-скриптом через EVAL/EVALSHA: Redis выполняет его целиком, чужие команды не вклиниваются. В Node.js это client.eval в node-redis или defineCommand в ioredis. Из готового: rate-limiter-flexible или связка express-rate-limit с rate-limit-redis.
Дальше детали рабочего решения: ответ 429 с заголовком Retry-After, ключ по id пользователя или API-ключу, а не только по IP, и осознанный выбор, что делать при падении Redis: пропускать всех или отказывать всем. В Redis Cluster ключи одного скрипта должны попасть в один слот, для этого часть ключа берут в фигурные скобки (hash tag).
Где спотыкаются
Вызывают INCR и EXPIRE на каждый запрос. TTL продлевается при каждом обращении, и активный клиент не выходит из блокировки никогда. А если процесс упадёт между двумя командами без MULTI, ключ останется без срока жизни.
Делают «прочитать, сравнить, записать» в коде Node.js через GET и SET. Между ними успевают пройти параллельные запросы, и лимит пробивается.
Берут req.ip за балансировщиком без app.set('trust proxy', ...). Тогда у всех пользователей один адрес балансировщика, и лимит срабатывает на всех сразу.