Redis — это не «база данных в памяти», а универсальный швейцарский нож для бота: FSM, кэш, очередь, rate limit, временные ссылки, distributed lock, pub/sub, аналитика в реальном времени. Под капотом — однопоточный event loop с микросекундной задержкой и десятком встроенных структур данных, каждая из которых решает отдельную задачу. Разберём, под какие сценарии его подключают, какие структуры выбирать и где он становится узким местом.
Зачем Redis в боте
Бот — это процесс с большим количеством мелких быстрых операций. Каждое сообщение требует чтения состояния, кэша горячих данных, проверки лимитов. SQL-база справляется, но добавляет 10–50 ms задержки на каждую операцию. Redis в этом сценарии — на порядок быстрее (под микросекунды) и выживает на одной машине под десятками тысяч RPS.
Поэтому в типовой архитектуре бота Redis закрывает «горячий путь», а Postgres — «холодное» хранение пользовательских данных, заказов, истории. Список задач, которые Redis закрывает в боте:
- FSM storage — состояния диалога между апдейтами.
- Rate limit — антифлуд и квоты на платные действия.
- Кеш — каталог, прайс, профили, ответы LLM.
- Очередь — фоновые задачи (рассылки, генерация PDF, тяжёлые AI-вызовы).
- Sessions — временные сессии после OAuth, magic-link, оплаты.
- Distributed lock — критичные операции (бронирование слота, списание баллов).
- Pub/Sub — broadcasting между инстансами бота и сервисами.
- Аналитика real-time — DAU, уникальные посетители, лидерборды.
Структуры данных и где их применять
Redis — не «key-value», а многоструктурное хранилище. Для каждой задачи в боте есть подходящая структура. Таблица:
| Структура | Команды | Применение в боте |
|---|---|---|
| String | GET / SET / INCR | Кеш ответов, счётчики, флаги, JWT-сессии |
| Hash | HSET / HGETALL | Профили пользователя (name, lang, premium) |
| List | LPUSH / BRPOP | Очередь рассылки, FIFO-задачи |
| Set | SADD / SISMEMBER | Кто видел акцию, реферальные множества |
| Sorted Set | ZADD / ZREVRANGE | Лидерборды, рейтинги, sliding-window лимиты |
| Streams | XADD / XREADGROUP | Журнал событий, replay, durable очередь |
| Bitmap | SETBIT / BITCOUNT | DAU/MAU, флаги по user_id |
| HyperLogLog | PFADD / PFCOUNT | Уникальные посетители (≈12 КБ на счётчик) |
| Geo | GEOADD / GEOSEARCH | Поиск ближайших точек, доставки |
Главное правило: не лепите всё в String через JSON. Если объект многократно частично читается — это Hash. Если нужна сортировка по очкам — Sorted Set. Если уникальность важнее счёта — Set или HyperLogLog.
FSM-стейт
Самый частый сценарий. У всех популярных фреймворков (aiogram, grammY, python-telegram-bot) есть готовый Redis Storage для FSM. Состояние и временные данные диалога хранятся в Redis с TTL.
Ключи у aiogram имеют формат `fsm:{bot_id}:{chat_id}:{user_id}:state` и `fsm:{bot_id}:{chat_id}:{user_id}:data`. TTL — 24–72 часа для обычных воронок. Память — ничтожная (десятки байт на пользователя), даже миллион активных пользователей укладывается в гигабайт.
Подключение в aiogram 3:
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.redis import RedisStorage
from redis.asyncio import Redis
redis = Redis(host="redis", port=6379, db=0, decode_responses=True)
storage = RedisStorage(redis=redis, state_ttl=86400, data_ttl=86400)
dp = Dispatcher(storage=storage)
Если у вас несколько ботов в одном Redis-инстансе — разнесите их по db (0..15) или используйте key prefix. Иначе при сбросе FSM одного бота легко зацепить чужие ключи.
Rate limit: token bucket на Lua
Telegram ограничивает: 30 сообщений в секунду на бота, 1 сообщение в секунду на чат, 20 в минуту в группу. Свой rate limit — отдельная история: защита от DoS, ограничение «бесплатных» AI-запросов, антифлуд.
Простейший вариант — INCR ключа `ratelimit:{user_id}:{bucket}` и EXPIRE на окно. Но этот подход страдает от race condition между INCR и EXPIRE (если процесс упал между ними — ключ останется без TTL). Решение — атомарный Lua-скрипт:
-- token_bucket.lua
-- KEYS[1] = ключ ведра, ARGV[1] = capacity, ARGV[2] = window_sec
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[2]))
end
if current > tonumber(ARGV[1]) then
return 0
end
return 1
Вызов из Python:
ALLOW = await redis.eval(SCRIPT, 1, f"rl:user:{user_id}:min", 30, 60)
if not ALLOW:
await message.answer("Слишком часто, подождите минуту")
return
Для sliding window используйте Sorted Set: ZADD с score=now(), затем ZREMRANGEBYSCORE старше окна, ZCARD для подсчёта. Точнее, чем фиксированное окно, но дороже (O(log N) на запись).
Distributed lock
Бронирование слота, списание бонусов, выдача промокода — операции, которые не должны исполниться дважды. Когда у вас несколько инстансов бота за nginx, без блокировки два пользователя одновременно «купят» последний билет.
Базовый паттерн — SET key value NX EX 10:
import secrets
lock_token = secrets.token_hex(16)
acquired = await redis.set(
f"lock:slot:{slot_id}", lock_token,
nx=True, ex=10,
)
if not acquired:
await message.answer("Слот занимают, попробуйте через секунду")
return
try:
await book_slot(slot_id, user_id)
finally:
# снимаем только свой лок (Lua, чтобы атомарно)
await redis.eval(
"if redis.call('get', KEYS[1]) == ARGV[1] "
"then return redis.call('del', KEYS[1]) else return 0 end",
1, f"lock:slot:{slot_id}", lock_token,
)
Ключевые моменты: NX — взять только если ключа нет, EX 10 — TTL чтобы лок не повис при падении процесса, value — случайный токен чтобы не снять чужой лок при истечении своего. Для критичных систем — RedLock на 5 нод, но для бота обычно избыточно.
Кеш горячих данных
Каталог товаров, прайс-лист, настройки тарифов — часто читаются, редко меняются. Класть их в Postgres каждый раз — расточительство. Кэшируем в Redis с TTL 5–15 минут или с явной инвалидацией при обновлении.
Паттерны:
- Cache-aside: смотрим в Redis, если нет — читаем БД и кладём в Redis.
- Write-through: запись идёт сразу и в БД, и в Redis.
- Write-behind: запись сначала в Redis, потом фоном в БД.
Для бота 95% случаев — cache-aside. Это просто и предсказуемо.
Кеш ответов LLM
Если бот ходит в OpenAI/Anthropic/YandexGPT — каждый запрос стоит денег и занимает 2–10 секунд. На повторяющихся промптах (FAQ, стандартные команды) кеш экономит 60–90% бюджета.
Ключ — SHA-256 от нормализованного промпта (model + temperature + system + user):
import hashlib, json
def llm_cache_key(model: str, system: str, user: str, temp: float) -> str:
payload = json.dumps([model, system, user, temp], ensure_ascii=False)
digest = hashlib.sha256(payload.encode()).hexdigest()[:32]
return f"llm:{model}:{digest}"
async def ask_llm(prompt: str) -> str:
key = llm_cache_key("gpt-4o-mini", SYSTEM, prompt, 0.0)
cached = await redis.get(key)
if cached:
return cached
answer = await openai_call(prompt)
await redis.set(key, answer, ex=86400 * 7)
return answer
Кешировать имеет смысл только при temperature=0 (детерминированный ответ). Для творческих режимов (temp > 0.5) кеш бессмысленен.
Очереди задач
Тяжёлые операции (генерация PDF, отправка уведомлений всему списку, вызов медленного AI) нельзя делать в обработчике — Telegram отвалит webhook за 60 секунд. Решение — фоновая очередь.
Варианты на Redis:
- RQ (Python) — простая очередь, быстро ставится, минимум зависимостей.
- Celery с Redis backend — более тяжёлый, но богат фичами (расписания, retry, цепочки).
- BullMQ (Node) — отраслевой стандарт для TypeScript-ботов.
- Redis Streams + свой воркер — если нужна полная гибкость и at-least-once.
Обработчик кладёт задачу в очередь, мгновенно отвечает Telegram «принято», воркер выполняет в фоне и присылает ответ пользователю отдельным сообщением.
Минимальный пример на List:
# producer внутри хендлера
await redis.lpush("queue:broadcast", json.dumps({
"user_id": user_id, "text": text, "ts": time.time(),
}))
# worker.py
while True:
_, payload = await redis.brpop("queue:broadcast", timeout=0)
job = json.loads(payload)
await bot.send_message(job["user_id"], job["text"])
BRPOP блокирующий — воркер не молотит CPU вхолостую. Для надёжности (at-least-once) переходите на Streams с XREADGROUP и явным XACK.
Pub/Sub и broadcasting
Redis Pub/Sub — лёгкий способ распределить событие между инстансами бота или сервисами. Например: оплата прошла → платёжный сервис публикует order:paid → бот подписан на этот канал → отправляет пользователю чек.
Альтернатива — Streams (надёжнее, поддерживает consumer groups, replay). Pub/Sub проще, но не гарантирует доставку при простое подписчика — это «fire and forget».
Используйте Pub/Sub для: cache invalidation между инстансами, сигналов «обнови конфиг», live-обновления админки. Для бизнес-событий (оплата, регистрация) — Streams.
Sessions и одноразовые токены
В оплате через сторонние сервисы часто нужен временный токен «вернуть пользователя в правильное место». Redis с TTL 10 минут — идеальное место: ключ `payment:{token}`, значение — payload с ID пользователя и заказа.
То же с одноразовыми ссылками («скачать файл по ссылке в течение часа»), кодами подтверждения, magic-links, OAuth state-параметром.
Хранение больших callback_data
Telegram ограничивает callback_data 64 байтами. Если у вас сложный callback (например, «купить товар X в количестве Y по цене Z с доставкой W»), 64 байт не хватит.
Решение — хранить полный payload в Redis и в callback_data класть короткий ключ:
import secrets
token = secrets.token_urlsafe(8) # 11 символов
await redis.set(f"cb:{token}", json.dumps(payload), ex=3600)
button = InlineKeyboardButton(text="Купить", callback_data=f"buy:{token}")
# в хендлере
@dp.callback_query(F.data.startswith("buy:"))
async def buy(call: CallbackQuery):
token = call.data.split(":", 1)[1]
raw = await redis.get(f"cb:{token}")
if not raw:
await call.answer("Кнопка устарела", show_alert=True)
return
payload = json.loads(raw)
Тот же приём — для reply_to связей в длинных диалогах с LLM (хранить ID родительского сообщения и контекст).
Persistence: RDB vs AOF
По умолчанию Redis — в памяти. При рестарте всё пропадает. Для FSM это допустимо (TTL пересчитается, диалог потеряется), для критичных данных — нет.
- RDB — снапшот всей памяти на диск (например, каждые 5 минут). Компактный, быстрый рестарт. Минус — теряете данные между снапшотами.
- AOF — append-only log всех команд. Надёжнее (потеря максимум 1 секунды при
appendfsync everysec), но файл больше и рестарт дольше. - Гибрид — RDB + AOF одновременно (
aof-use-rdb-preamble yes). Рекомендуется в большинстве случаев.
Для бота: RDB каждые 15 минут + AOF с appendfsync everysec — хороший дефолт. Если в Redis только кеш — можно вообще без persistence.
Кластер vs Sentinel vs single
- Single — один инстанс. До 10–50 тысяч активных пользователей хватает. Простая настройка, никакого оверхеда.
- Sentinel — один master + 1–2 replica + 3 sentinel-ноды для failover. HA, но не масштабирует запись. Подходит, когда важна доступность, но объём данных умещается на одной машине.
- Cluster — шардирование на 3+ master + replicas. Нужен, когда не хватает RAM или write-нагрузки одной ноды. Сложнее: multi-key операции работают только в пределах одного слота (используйте
{hashtag}в ключах).
99% Telegram-ботов живут на single instance. Sentinel — когда uptime критичен (платёжный, банковский). Cluster — почти никогда.
Eviction policy
Redis может быть «безграничным» (вылетит по OOM) или принудительно вытеснять старые ключи при заполнении maxmemory. Политики:
noeviction— отказ записи при переполнении. Дефолт для FSM: лучше упасть, чем потерять состояния.allkeys-lru— выкидывать давно не использованные. Дефолт для кеша.volatile-lru— то же, но только среди ключей с TTL.allkeys-lfu— по частоте использования (точнее LRU для длинных хвостов).
Если в одном Redis и FSM, и кеш — разнесите по разным db или инстансам с разными политиками. Иначе кеш сожрёт FSM.
Мониторинг
Минимум, что должно быть:
redis-cli INFO memory—used_memory,mem_fragmentation_ratio. Если фрагментация > 1.5 — стоит включитьactivedefrag yes.redis-cli INFO stats—instantaneous_ops_per_sec,keyspace_hits/keyspace_misses(hit ratio).SLOWLOG GET 10— медленные команды (порогslowlog-log-slower-than 10000микросекунд).LATENCY DOCTOR— диагностика всплесков задержки.- Экспорт в Prometheus через
redis_exporter, дашборд в Grafana.
Алерты: память > 80%, hit ratio < 70%, evicted_keys растёт, replication lag > 1s.
Подводные камни в продакшене
KEYS *— никогда. Блокирует Redis на всё время сканирования (минуты на больших базах). ИспользуйтеSCANс курсором.- Большие
MGET/HGETALL. Команда исполняется атомарно, блокирует event loop. Если в hash 100 тысяч полей — это десятки миллисекунд блокировки. Бейте на чанки. - Hot keys. Один ключ, в который ломятся все, — bottleneck. Шардируйте (
counter:{user_id % 16}) или кешируйте локально с коротким TTL. - Большие values. Redis любит мелкие значения. JSON > 1 МБ — повод подумать о другой БД или сжатии.
FLUSHDBв продакшене. Закройтеredis-cliдоступ паролем (requirepass) и переименуйте опасные команды черезrename-command FLUSHDB "".- Pub/Sub без consumer. Сообщения теряются молча. Для надёжной доставки — Streams.
- TLS и
requirepass. Если Redis выставлен в интернет (не делайте так) — обязательно. Лучше держать в private network.
Альтернативы Redis
В 2024–2026 экосистема разделилась после смены лицензии Redis на SSPL/RSAL:
- Valkey — форк Redis от Linux Foundation (BSD), API-совместим. Дефолт для большинства Linux-дистрибутивов. Если ставите «Redis» в Ubuntu 24.04+ — это Valkey.
- KeyDB — мульти-тредовый форк, выше throughput на жирных серверах. Активность снизилась после покупки Snap.
- DragonflyDB — переписан с нуля на C++, до 25× быстрее на benchmark. API-совместим. Подходит, когда упёрлись в одно ядро Redis.
- Garnet от Microsoft — на .NET, экзотика для бота.
Для типового Telegram-бота переезжать с Redis/Valkey смысла нет. Думать об альтернативах — только когда упёрлись в потолок одной ноды.
Когда Redis избыточен
Не каждому боту нужен Redis. Если:
- Меньше 100 пользователей в день.
- Один инстанс, нет горизонтального масштабирования.
- FSM-стейт можно держать в памяти (короткие диалоги, перезапуски редки).
…тогда in-memory storage достаточно. Redis — это уровень, на который вы переходите при первых признаках роста: несколько инстансов, рестарты теряют состояние, нужны очереди и rate limit.
Итого
Redis в Telegram-боте — это FSM, кэш, очереди, rate limit, distributed lock, pub/sub и одноразовые токены в одном инструменте. Под каждую задачу — своя структура: Hash для профилей, Sorted Set для лидербордов и sliding window, Streams для надёжных очередей, Bitmap и HyperLogLog для аналитики. Один инстанс с 1–2 ГБ памяти закрывает потребности бота на сотни тысяч пользователей. Подключать его стоит на этапе перехода с MVP в продакшен — не раньше, но и не сильно позже.
Частые вопросы
Зачем нужен Redis в Telegram-боте?
Бот — это процесс с большим количеством мелких быстрых операций. Каждое сообщение требует чтения состояния, кэша горячих данных, проверки лимитов. SQL-база справляется, но добавляет 10–50 ms задержки на каждую операцию. Redis в этом сценарии — на порядок быстрее (под микросекунды) и выживает на одной машине под десятками тысяч RPS. В типовой архитектуре бота Redis закрывает «горячий путь» (FSM, кэш, лимиты, очереди, locks, pub/sub), а Postgres — «холодное» хранение пользовательских данных, заказов, истории.
Как использовать Redis для FSM Telegram-бота?
У всех популярных фреймворков (aiogram, grammY, python-telegram-bot) есть готовый Redis Storage для FSM. У aiogram 3 ключи имеют формат fsm:bot_id:chat_id:user_id:state и :data, TTL — 24–72 часа для обычных воронок. Память — десятки байт на пользователя, миллион активных пользователей укладывается в гигабайт. При рестарте бота состояние сохраняется (если включён persistence), и пользователи продолжают диалог с того же шага. Eviction policy для FSM — обязательно noeviction, иначе кеш в том же инстансе вытеснит состояния.
Какие структуры данных Redis применять в боте?
String — кеш ответов, счётчики, флаги. Hash — профили пользователя (имя, язык, premium-статус). List — простые очереди через LPUSH/BRPOP. Set — множества (кто видел акцию, рефералы). Sorted Set — лидерборды и sliding-window rate limit. Streams — надёжные очереди с at-least-once и replay. Bitmap — DAU/MAU с экономной памятью. HyperLogLog — уникальные посетители (≈12 КБ на счётчик). Главное правило: не лепите всё в String через JSON — выбирайте структуру под операции.
Как сделать rate limit и distributed lock на Redis?
Rate limit — Lua-скрипт с INCR + EXPIRE атомарно (фиксированное окно), либо Sorted Set с ZADD score=now и ZREMRANGEBYSCORE для sliding window. Distributed lock — команда SET key token NX EX 10: NX берёт ключ только если нет, EX даёт TTL чтобы лок не повис, token (случайный) нужен чтобы при освобождении не снять чужой лок (через Lua-сравнение). Для критичных систем — RedLock на 5 нод, но для типового бота избыточно. Lock нужен для бронирования слотов, выдачи промокодов, списания баллов в многоинстансовом боте.
Как кешировать ответы LLM через Redis?
Ключ — SHA-256 от нормализованного промпта (model + temperature + system + user), значение — текст ответа, TTL — 1–7 дней. Имеет смысл только при temperature=0 (детерминированный ответ); для творческих режимов кеш бессмысленен. На повторяющихся запросах (FAQ, стандартные команды) экономия 60–90% бюджета OpenAI/Anthropic. Тем же приёмом храните callback_data больше 64 байт: в callback_data кладёте короткий токен, полный payload — в Redis с TTL час.
RDB vs AOF — какой persistence выбрать для бота?
RDB — снапшот всей памяти на диск (каждые 5–15 минут). Компактный, быстрый рестарт, но теряете данные между снапшотами. AOF — append-only log всех команд, надёжнее (потеря максимум 1 секунды при appendfsync everysec), но файл больше. Гибрид RDB + AOF (aof-use-rdb-preamble yes) — рекомендуется в большинстве случаев. Для бота: RDB каждые 15 минут + AOF everysec — хороший дефолт. Если в Redis только кеш — можно вообще без persistence.
Какие есть подводные камни Redis в продакшене?
KEYS * — никогда, блокирует Redis на минуты, используйте SCAN. Большие MGET/HGETALL блокируют event loop — бейте на чанки. Hot keys (один ключ, куда ломятся все) — шардируйте по user_id % N. Большие values (>1 МБ JSON) — повод подумать о другой БД или сжатии. FLUSHDB закройте через rename-command. Pub/Sub теряет сообщения молча — для надёжной доставки используйте Streams. Мониторьте hit ratio, evicted_keys, slowlog, latency через redis_exporter и Grafana.