← Cache & Workers

Redis: инстансы и ключи

cache-workers · workzone

Топология Redis и единый реестр ключей: какие физические инстансы держит модуль, какие ключи в них живут и сколько. Опирается на ось «система записи vs быстрый производный» — водораздел ниже.

Водораздел: Redis ⟂ Postgres

Ось — система записи vs быстрый производный. Postgres держит систему записи и корректностно-критичное взаимное исключение (единственность прогона — транзакционно, рядом с журналом). Redis держит быстрые / истекающие / in-flight производные: кэш (пересоберётся), счётчики rate-limit (durable — потеря открыла бы окно перебора), дедуп, полезную нагрузку очереди (восстановима из журналов). Это осознанный выбор всех модулей, не случайность. Отсюда же — отказ от Redlock: грубые редкие замки держит Postgres, распределённый лок-сервис не нужен и без fencing-токенов опасен для корректности. Мгновенный отзыв access-токена через jti-blacklist в Redis — v2: в v1 access-токен stateless и гаснет сам по сроку жизни.
Redis — эфемерное

потеря болезненна, но восстановима темпом

  • полезная нагрузка очереди
  • счётчики rate-limit durable
  • blacklist отозванных access-токенов v2
  • дедуп webhook / job
  • кэш кандидатов поиска
Postgres — долговечное

источник истины, терять нельзя

  • единственность прогона (партиал-UNIQUE + heartbeat)
  • журналы прогонов · DLQ сбойных задач
  • бюджеты AI (SQL-агрегат, не счётчик)
  • сессии · refresh-токены · история диалога

Два инстанса

Кэш и долговечное — в разных инстансах. В одном инстансе с авто-вытеснением по памяти их держать нельзя: вытеснение не различает расходный кэш и данные, терять которые нельзя, — под давлением оно может молча выкинуть задачу из очереди, счётчик rate-limit или blacklist отозванного токена (дыра в безопасности). Поэтому роли разведены физически — два процесса с разной политикой памяти, а не два namespace в одном.

Доступ. Оба инстанса — только во внутренней сети, наружу не публикуются; вход по паролю (requirepass), в проде канал под TLS. В Redis лежат счётчики rate-limit и blacklist токенов v2 — читать их извне нельзя.

redis-durable
maxmemory + noeviction · AOF-персист · переживает рестарт
  • очередь SAQ
  • счётчики rate-limit
  • jti-blacklist v2
  • дедуп-ключи
  • координационный замок — maintenance-режим · restore KS
  • Терять нельзя — память не вытесняется, состояние на диске.
  • Потолок — отказ записи, не вытеснение: при достижении maxmemory политика noeviction отклоняет новую запись с ошибкой, а не выкидывает данные. Продьюсеры упираются в отказ и тормозят (backpressure) — инстанс не растёт без предела и не уходит под OOM-killer. Глубину очереди всё равно держим под мониторингом, чтобы до потолка не доходить.
redis-cache
maxmemory + allkeys-lru · без персиста
  • кэш кандидатов поиска
  • pub/sub-канал push-уведомлений
  • Растёт без предела — вытеснение обязательно.
  • Каждый ключ с TTL; персист не нужен (всё пересоберётся).
  • pub/sub — эфемерный сигнал, не ключ; пропажа лечится опросом.

Эфемерные ключи (TTL)

Часть состояния живёт ровно столько, сколько окно, в которое оно что-то значит — TTL сам убирает ключ, отдельной уборки не нужно.

jti-blacklist v2 Мгновенный отзыв access-токена — задел на v2. Ключ будет жить ровно время жизни токена: позже он истёк бы сам, держать незачем. В v1 досрочного отзыва нет — access-токен stateless, гаснет по сроку. Потребитель — Auth & Security.
дедуп webhook Одно событие — одна доставка. Отметка приёма не даёт повторно обработать ту же входящую доставку в окне.
окно идемпотентности Одна постановка job_id — один прогон. Окно отсекает дубль-постановку той же задачи; полная механика — на единственности.

Реестр ключей

Единая конвенция префиксов: namespace по двоеточию задаёт инстанс, срок жизни и владельца. Новый ключ — новая строка здесь, не свободное имя в коде.

Namespace Назначение Инстанс TTL Владелец
q: очередь задач — полезная нагрузка прогонов durable до завершения очереди
rl: счётчики rate-limit (token-bucket · скользящее окно) durable окно лимита rate-limit
brute: состояние brute-force барьера — окно по IP, счётчик и задержка по аккаунту, попытки кода привязки durable окно барьера Auth & Security
grace: грация ротации refresh — старый токен → новая пара на ~10 с durable окно грации Auth & Security
bl: v2 blacklist отозванных jti durable lifetime access-токена Auth & Security
cache: кэш кандидатов поиска cache per-key (+ LRU) кэш
lock: координационный замок для обслуживания — один держатель одновременно (maintenance-режим · restore KS) durable на время операции прогоны
dedup:webhook: дедуп входящих webhook-доставок — одно событие, одна обработка durable окно приёма Harvester
dedup:slack-event: дедуп входящих событий Slack-бота — повтор не отвечает дважды durable окно приёма Slack
dedup:telegram-update: идемпотентность повторов апдейтов Telegram (по update_id) — повтор не отвечает дважды durable окно приёма Telegram
telegram:active-conv:chat_id указатель активной беседы для chat_id — реплики продолжают её; /new его сбрасывает, следующее сообщение откроет новую (дом истины беседы — Postgres conversations) cache скользящий, на время беседы Telegram
cache:mattermost:listener флаг здоровья WebSocket-слушателя Mattermost — heartbeat-JSON; истёкший ключ читается как «не запущен» (производное состояние — терять безопасно) cache 90 с Mattermost
dedup:job: идемпотентность постановки job_id — один прогон durable ~24ч (успешный прогон) жизненный цикл
push: pub/sub-канал — сигнал новой строки в ленте cache эфемерный (pub/sub) Notifications

Память кэша. maxmemory — через env: dev 256mb · prod 512mb, потолок ≤ 70 % памяти контейнера. Подбор по метрикам в работе (keyspace_hits/misses, evicted_keys), не фиксированным числом — вмещаем горячее рабочее множество, не все запросы. Тюнинг по метрикам — про redis-cache; у redis-durable тот же maxmemory служит жёстким потолком с отказом записи (noeviction), а не вытеснением.