← назад к разделу

Redis чаще всего появляется в проекте как кэш. Но те же самые структуры данных и атомарные операции решают ещё несколько классических задач бэкенда — без подключения отдельных сервисов.

Зачем вообще идти дальше кэша

Когда приложение работает в нескольких экземплярах, привычные инструменты начинают отказывать. Synchronized-блок защищает только один JVM-процесс. AtomicLong живёт только в памяти одного Pod'а. EventBus из Guava не знает о соседних серверах.

Redis работает как единая точка координации: все экземпляры приложения видят одно и то же состояние, а операции над ним атомарны. Это и есть фундамент для паттернов, которые разберём ниже.

Распределённая блокировка

Проблема

Несколько экземпляров приложения одновременно запускают один и тот же джоб или обрабатывают одно и то же событие. Нужно гарантировать, что только один экземпляр выполнит критическую секцию.

SETNX + TTL: простой вариант

SETNX (SET if Not eXists) — атомарная команда: ключ создаётся, только если его нет.

# Попытка захватить блокировку на 30 секунд
SET lock:invoice:42 owner-uuid-1234 NX EX 30
# OK — блокировка захвачена
# (nil) — кто-то другой уже держит блокировку

NX — «только если не существует», EX 30 — TTL в секундах. TTL обязателен: если процесс упадёт, блокировка освободится автоматически.

Освобождать нужно только свою блокировку — поэтому в значение пишем уникальный идентификатор владельца и проверяем его перед удалением:

# Проверить владельца и удалить — атомарно через Lua
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:invoice:42 owner-uuid-1234

Fencing-токен

Простой SETNX-замок имеет слабость: если процесс завис, TTL истечёт, другой экземпляр захватит блокировку — а первый «очнётся» и решит, что всё ещё держит её. Для критичных операций добавляют fencing-токен — монотонно возрастающее число, которое передаётся в каждый запрос к защищаемому ресурсу. Ресурс отклоняет запросы со старым токеном.

В Redis такой счётчик можно хранить командой INCR.

Redlock и его критика

Redlock — алгоритм распределённой блокировки поверх нескольких независимых Redis-узлов. Защищает от ситуации, когда единственный Redis-узел недоступен.

Коротко о спорах: Мартин Клеппманн показал, что Redlock не даёт строгих гарантий при остановках процесса (GC-паузах, network-partition). Авторы Redis считают, что для большинства реальных случаев Redlock достаточен. Вывод: если нужна абсолютная корректность (финансовые транзакции) — используйте fencing-токен или специализированный сервис (ZooKeeper, etcd). Для задач вроде «не запускать джоб дважды» Redlock подходит.

Готовая реализация для Java — библиотека Redisson (RLock).

Ограничение запросов (rate limiting)

Счётчик с TTL

Простейший rate limiter: считать запросы за период и блокировать, когда лимит превышен.

# Каждый запрос пользователя user:42
INCR rate:user:42:2024060312   # ключ = пользователь + час
EXPIRE rate:user:42:2024060312 3600

Если значение счётчика превысило лимит — возвращаем 429. TTL гарантирует, что ключ удалится через час.

Минус: fixed window — в конце и начале окна можно «удвоить» лимит. В последнюю секунду окна и первую следующего пройдёт в два раза больше запросов.

Скользящее окно (sliding window)

Точнее, но требует больше памяти. Для каждого запроса сохраняем метку времени в Sorted Set (ZADD), а перед проверкой удаляем устаревшие записи (ZREMRANGEBYSCORE):

# ts = текущая метка в миллисекундах
ZADD rate:user:42 1717425600000 "req-uuid"
ZREMRANGEBYSCORE rate:user:42 0 1717425600000-60000  # удалить старше 60 сек
ZCARD rate:user:42                                   # сколько запросов в окне
EXPIRE rate:user:42 60

Если ZCARD > лимита — отказ. Весь pipeline выполняется атомарно через MULTI/EXEC или Lua-скрипт.

В Spring Boot встроенная поддержка — через RedisTemplate или библиотеку Bucket4j с Redis-бэкендом.

Pub/Sub: публикация и подписка

Pub/Sub — канал для отправки сообщений всем подписчикам в реальном времени.

# Подписчик
SUBSCRIBE channel:notifications

# Издатель (из другого клиента)
PUBLISH channel:notifications "user:42 logged in"

В Spring Data Redis — RedisMessageListenerContainer с MessageListener.

Когда Pub/Sub подходит, а когда нет

Pub/Sub подходит для задач, где потеря одного сообщения некритична: обновление UI в реальном времени, сброс кэша на нескольких серверах, рассылка уведомлений «best-effort».

Pub/Sub не подходит, если нужно:

  • гарантировать доставку (подписчик, не слушавший в момент публикации, сообщение не получит);
  • переиграть историю событий;
  • обработать сообщение ровно один раз.

Для этих случаев — Redis Streams.

Redis Streams: надёжная очередь

Streams появились в Redis 5.0 как append-only журнал — аналог Kafka, но без отдельного кластера.

# Публикация события
XADD orders * order_id 1001 status created

# Чтение группой потребителей
XREADGROUP GROUP order-processor consumer-1 COUNT 10 BLOCK 2000 STREAMS orders >

# Подтверждение обработки
XACK orders order-processor 1717425600000-0

> означает «дай мне новые сообщения, которые ещё не взял никто в группе». После обработки вызываем XACK — без этого сообщение останется в состоянии «в обработке» и будет видно через XPENDING.

Streams дают:

  • сохранность сообщений (сообщение не пропадает при отключении потребителя);
  • группы потребителей с балансировкой нагрузки;
  • повторную обработку недоставленных сообщений.

В Java/Spring — StreamMessageListenerContainer из Spring Data Redis.

Streams vs Kafka: когда что выбрать

СитуацияRedis StreamsKafka
Redis уже есть в стекеподходитизбыточен
Нужно партиционирование на сотни топиковне оптимальноподходит
Объём — миллионы сообщений/секограничено RAMподходит
Replay за несколько дней/недельограничено политикойподходит
Простота эксплуатации важнее масштабаподходитсложнее

Короткая формула: Redis Streams — разумный выбор, когда Redis уже есть, а объём и требования к хранению укладываются в его возможности.

Коротко

  • Распределённая блокировкаSET key value NX EX ttl; владелец идентифицируется уникальным значением; освобождение через Lua-скрипт.
  • Fencing-токен — монотонный счётчик (INCR) как защита от «проснувшегося» старого замка.
  • Redlock подходит для большинства задач координации, но не для операций, где нужна строгая корректность при GC-паузах.
  • Rate limiting: простой счётчик с TTL (fixed window) или Sorted Set (sliding window) для точного скользящего окна.
  • Pub/Sub — доставка «best-effort»: нет гарантии доставки, нет истории. Хорош для сброса кэша и real-time уведомлений.
  • Streams — надёжная очередь с группами потребителей и подтверждением; выбирайте, когда Redis уже есть и Kafka избыточен.

Что почитать дальше

  • Структуры данных Redis — List, Sorted Set, Hash, Bitmap — основа для всех паттернов выше.
  • Паттерны кэширования — cache-aside, write-through, TTL и протухание записей.
  • Эксплуатация Redis — репликация, Redis Sentinel, Cluster, RDB и AOF.