Как обеспечить атомарность операций при увеличении счетчика запросов в Redis
Что проверяют
Понимаете ли вы, где на самом деле возникает гонка: не внутри Redis, а между несколькими инстансами Node.js, которые читают значение, прибавляют единицу и записывают обратно. Следом почти всегда спрашивают про rate limiter: как атомарно увеличить счётчик и поставить ему TTL.
Как отвечать
Основа: Redis выполняет команды по одной в одном потоке, поэтому каждая отдельная команда атомарна. Отсюда способы:
INCR,INCRBY,HINCRBY: увеличение за один вызов, сервер сразу возвращает новое значение. Вioredisиnode-redisэтоawait redis.incr(key). Для простого счётчика этого достаточно;MULTIиEXEC: команды ставятся в очередь и выполняются подряд, без чужих команд между ними. Отката нет: если одна команда упала при выполнении, остальные всё равно применятся. Для условной логики добавляютWATCH: если ключ успели поменять,EXECвернёт отказ и операцию нужно повторить;- Lua-скрипт через
EVALилиEVALSHA: скрипт исполняется целиком атомарно, внутри можно ветвиться по результату. В Redis 7 для того же появились Functions (FUNCTION LOAD,FCALL).
Главная ловушка в лимитере: сначала INCR, потом отдельным вызовом EXPIRE. Если процесс упадёт между ними, ключ останется без TTL, и пользователь окажется заблокирован навсегда. Надёжные варианты: MULTI с INCR и EXPIRE key 60 NX (опция NX у EXPIRE есть с Redis 7.0 и ставит TTL, только если его ещё нет) или короткий Lua-скрипт:
local c = redis.call('INCR', KEYS[1]) if c == 1 then redis.call('EXPIRE', KEYS[1], ARGV[1]) end return c
Где спотыкаются
Делают GET, прибавляют единицу в JavaScript и делают SET. На одном процессе это незаметно, а под нагрузкой с несколькими подами счётчик теряет обновления.
Считают MULTI полноценной транзакцией с откатом, как в SQL. В Redis это только гарантия, что команды не перемешаются с чужими.
Пишут тяжёлые Lua-скрипты. Пока скрипт работает, Redis не обслуживает остальных клиентов, а в Redis Cluster все ключи скрипта обязаны лежать в одном слоте, иначе будет ошибка CROSSSLOT.