Как реализовать счетчик с временем жизни ключа в Redis для задачи автоотклонения звонков
Что проверяют
Умеете ли вы сделать ограничение по частоте атомарно и понимаете ли, как Redis на самом деле удаляет ключи по TTL. Задача обычно звучит так: если с номера пришло больше N звонков за минуту, следующие отклоняем. Следом спрашивают про гонки и границы окна.
Как отвечать
Базовый вариант, фиксированное окно:
- ключ:
calls:{номер}или с номером окна,calls:{номер}:{минута}; - счётчик:
INCRатомарен и создаёт ключ со значением 1, если его не было. Уже заданный TTL он не сбрасывает; - время жизни: TTL ставится только при создании окна. Надёжно это делается одной транзакцией: в redis-py
pipeline()сincr(key)иexpire(key, 60, nx=True). ОпцияNXуEXPIREпоявилась в Redis 7.0, на старых версиях то же самое пишут Lua-скриптом черезEVAL; - решение: если вернувшееся значение больше порога, звонок отклоняется сразу, в том же запросе.
Затем назовите слабость: на стыке двух окон абонент успевает сделать почти 2N звонков. Если это критично, берут скользящее окно на сортированном множестве: ZADD с меткой времени, ZREMRANGEBYSCORE для старых записей, ZCARD для подсчёта, всё в одном скрипте. Памяти уходит больше, зато граница честная.
Для data-ml важна вторая сторона: такой счётчик это онлайн-признак для антифрод-модели, а порог N подбирается по распределению исторических звонков, а не на глаз. Ложное отклонение нормального абонента стоит дорого, поэтому порог стоит проверить на данных до выкатки.
Где спотыкаются
Делают INCR и EXPIRE двумя отдельными вызовами. Если процесс упадёт между ними, ключ останется без TTL, и номер будет заблокирован навсегда.
Вызывают EXPIRE на каждом звонке. Окно сдвигается при каждом обращении, и настойчивый абонент не выйдет из блокировки, пока продолжает звонить.
Строят отклонение на истечении ключа: ждут через sleep или подписываются на keyspace notifications. Redis удаляет просроченные ключи лениво и фоновой выборкой, событие приходит не ровно в момент TTL, а pub/sub не гарантирует доставку.