Пора перестать воспринимать Redis как временное хранилище key-value. Сегодня это многофункциональный движок, объединяющий в себе брокер сообщений с поддержкой Pub/Sub, гео-БД с Spatial-индексами, и надежный инструмент синхронизации через распределенные блокировки (Redlock). Современный Redis, потенциально, может заменить стек из 3–4 узкоспециализированных сервисов.

В новом переводе от команды Spring АйО рассмотрим, как использовать Redis на полную катушку и превратить его в фундамент вашей архитектуры.


«Большинство разработчиков впервые сталкиваются с Redis как с кэшем».
Вы ставите его перед базой данных, задаёте время жизни (TTL), наблюдаете, как время отклика падает с 500 мс до 50 мс, и идёте дальше… Redis тихо делает свою работу. Система становится быстрее. Для большинства команд на этом всё и заканчивается — Redis превращается в «ту самую штуку для кэширования», о которой никто не вспоминает, пока она работает.

И знаете, такая модель в голове вполне рабочая. Ваш API быстрее, база данных больше не плачет от перегрузки — все довольны.

Но вот в чём дело… относиться к Redis только как к кэшу — всё равно что купить Ferrari и ездить на ней только в супермаркет. Да, она быстро довезёт вас до магазина, но вы упускаете главное.

Поработав с Redis в продакшене достаточно долго, я понял: это один из самых недооценённых инструментов в нашем стеке. Redis — это не кэш, который просто оказался быстрым. Это сервер структур данных, который, ко всему прочему, отлично справляется с ролью кэша.

И это различие меняет всё.

Что такое Redis на самом деле…

Давайте чётко определим, с чем мы имеем дело.
Redis расшифровывается как REmote DIctionary Server — «удалённый сервер словарей». И здесь слово «словарь» играет ключевую роль. Да, Redis — это хранилище пар «ключ-значение» в оперативной памяти, но такое описание сильно занижает его возможности.

В большинстве систем хранимые значения — это непрозрачные блобы:

// Generic key-value store thinking
cache.set("user:1", JSON.stringify(userData));
Комментарий от Михаила Поливаха

Это довольно спорное утверждение. Хранить условные неструктурированные BLOB-ы в традиционных OLTP хранилищах типа MongoDB/PostgreSQL/Cassandra это кончено можно, но всё же это не от хорошей жизни. 

У этого есть ряд проблем, в сноску не поместится, но в идеале такие BLOB-ы хранить либо в каком-либо S3 и потом работать с ним как с Data Warehouse, возможно вам вполне хватит инструментов по-типу Amazon Athena. Либо ещё вариант - хранить в специальных high-performance файловых системах по-типу Elastic File System. Я работал в основном с AWS, поэтому, прошу прощения, что аналогии только из AWS, в других клаудах в т.ч. в РФ есть похожие аналоги.

База данных не знает и не заботится о том, что находится внутри этой строки. Вся логика — парсинг, обновление, валидация — происходит в коде вашего приложения.

С Redis всё иначе. Он предоставляет родные структуры данных и встроенные операции для работы с каждой из них:

  • строки

  • хэши (карты поле–значение)

  • списки

  • множества

  • отсортированные множества

  • потоки

  • геопространственные индексы

  • битмапы и HyperLogLog

Redis не просто хранит эти структуры — он умеет безопасно и атомарно их изменять.
И это не мелочь. Это и есть суть Redis.

Ментальный сдвиг: мышление о кэше vs мышление о Redis

Позвольте показать, что я имею в виду, на примере реального кода. Именно здесь у большинства происходит озарение.

Обычный подход к кэшированию


Вы хотите отслеживать количество подписчиков. Стандартный паттерн кэширования выглядит так:

// Read-modify-write with JSON caching (using ioredis)
const userData = await redis.get("user:1");
const user = JSON.parse(userData);
 
user.followers += 1;
 
await redis.set("user:1", JSON.stringify(user));

Этот подход работает — пока не перестаёт. При одновременных запросах два обращения могут считать одно и то же значение и перезаписать его друг друга. В результате вы теряете обновления.

Вот что происходит на самом деле:

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

Нативный подход с Redis


Теперь смоделируем те же данные, используя примитивы Redis:

// Let Redis own the state transition
await redis.hSet("user:1", "name", "Alex");
await redis.hSet("user:1", "followers", 123);
 
// Increment atomically, no race conditions possible
await redis.hIncrBy("user:1", "followers", 1);

Процесс становится гораздо чище:

Никакого цикла «чтение–модификация–запись»,
никаких гонок,
никаких блокировок на уровне приложения.

Redis гарантирует атомарность операции, потому что он однопоточен и обрабатывает команды по одной.

Комментарий от Михаила Поливаха

Тут не совсем правда. Redis не совсем однопоточен. Дело в том, что с целью убрать contention, координацию между потоками и т.д. сам "executioner" поток он реально один. Это может показаться странным, но на деле иметь один поток. который обрабатывает некую очередь из операций имеет преимущества, опять же, смотрите например на дизайн Netty и event loop-ов в ней - там по тем же причинам один поток занимается обработкой одного event loop-а и набора channel-ов в нем. 

Тем не менее, в редисе уже лет наверное 5-6 как есть вспомогательные треды, которые занимаются парсингом инструкций и т.п. Там архитектура системы сложная, опять же в сноску не поместиться, но я просто для общего развития решил пояснить

Этот сдвиг — передать управление переходом состояния Redis, а не использовать его как «тупое хранилище» — тонкий, но мощный.
Увидев его однажды, вы уже не сможете мыслить по-старому.

Почему Redis действительно быстрый (и в чём настоящие причины)

Все знают, что Redis — это быстро. Но мало кто понимает, почему. Здесь нет магии — есть конкретные архитектурные решения, которые обеспечивают такую производительность:

  • Ориентированность на память — доступ к оперативной памяти на порядки быстрее, чем к диску. Но дело не только в использовании RAM. Redis оптимизирует свои структуры данных под шаблоны доступа в памяти.

  • Простые операции — Redis сознательно не поддерживает сложные запросы, соединения (joins) или произвольные вторичные индексы. Большинство команд выполняются за O(1) или O(log n). Нет планировщика запросов, нет фазы оптимизации — только предсказуемые и быстрые операции.

  • Однопоточная модель исполнения — звучит как ограничение, пока не поймёшь, в чём здесь выгода. Redis обрабатывает команды последовательно, что означает отсутствие конкуренции за блокировки, отсутствие гонок внутри самого Redis, предсказуемые задержки и более простую реализацию с меньшим числом ошибок.

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

Я изучал некоторые бенчмарки на продакшн-API с нагрузкой в 10 000 запросов в секунду:

  • Запрос к PostgreSQL с корректно настроенными индексами: ~50 мс

  • Тот же запрос с использованием кэша Redis: ~5 мс

  • Операция с нативной структурой данных Redis: ~0,5 мс

Это не опечатка.
Ответы с задержкой меньше миллисекунды — это норма для Redis.

Redis как менеджер состояния


Вот здесь Redis начинает восприниматься не как кэш, а как полноценная инфраструктура.

Реальные счётчики и метрики в реальном времени


Один из самых простых, но при этом мощных паттернов:

// Track page views (simple counter)
await redis.incr("page:home:views");
 
// Track unique visitors with HyperLogLog (probabilistic counting)
// HyperLogLog uses ~12KB to track millions of unique items with ~0.8% error
await redis.pfAdd("page:home:visitors", userId);
const uniqueVisitors = await redis.pfCount("page:home:visitors");

Почему это лучше, чем Postgres? Потому что инкремент счётчика в Postgres требует:

  • захватить блокировку строки

  • прочитать значение

  • записать новое значение

  • освободить блокировку

  • записать в журнал WAL

  • возможно, обновить индекс

Комментарий от Михаила Поливаха

Автор тут не совсем прав. Запись в WAL происходит до любой UPDATE/DELETE/INSERT. В этом и суть WAL-а, это быстрая append only структура данных, куда попадает команда до выполнения.

Redis делает просто: INCR — готово.

Распределённое ограничение частоты (rate limiting)


Я усвоил этот урок на горьком опыте — после того, как бот-атака положила наш API в 2021 году. Нам понадобился механизм ограничения частоты запросов, который работал бы на нескольких серверах.

// Sliding window rate limiter using sorted sets
const userId = "123";
const now = Date.now();
const windowMs = 60000; // 1 minute
const maxRequests = 100;
 
// Remove old requests outside the window
await redis.zRemRangeByScore(`rate:${userId}`, 0, now - windowMs);
 
// Count requests in current window
const currentRequests = await redis.zCard(`rate:${userId}`);
 
if (currentRequests < maxRequests) {
  // Add this request to the window
  await redis.zAdd(`rate:${userId}`, { score: now, value: `${now}` });
  // Allow request
} else {
  // Rate limit exceeded
}
 
// Expire the key after the window to save memory
await redis.expire(`rate:${userId}`, Math.ceil(windowMs / 1000));
Комментарий от Михаила Поливаха

Для тех из людей, кто активно читает наши материалы напомню - помимо относительно частых алгоритмов в rate limiting-е запросов (Token Bucket / Leaky Bucket), есть ещё и алгоритмы с поддержкой либо: 

- фиксированного окна
- либо скользящего окна (как в данном например случае). 

Скользящее окно, которые работает на sorted set-ах, его часто как раз и реализуют на практике через ZSet в Redis.

Этот паттерн спас нам шкуру. Все серверы используют общее состояние ограничения частоты в Redis, поэтому пользователи не могут обойти лимиты, отправляя запросы на разные серверы.

Хранение сессий
Перестаньте использовать JWT для всего подряд. Иногда вам нужны полноценные сессии, которые можно немедленно инвалидировать.

// Store session with automatic expiration
await redis.setEx(
  `session:${sessionId}`,
  3600, // 1 hour TTL
  JSON.stringify({
    userId: "123",
    email: "user@example.com",
    lastActivity: Date.now(),
  }),
);
 
// Retrieve and extend session
const session = await redis.get(`session:${sessionId}`);
if (session) {
  await redis.expire(`session:${sessionId}`, 3600); // Reset TTL
}
 
// Immediate logout across all servers
await redis.del(`session:${sessionId}`);

Это как раз та область, где Redis действительно превосходит базы данных. Обращения к сессиям происходят постоянно, и вам нужна максимальная скорость.
К тому же, встроенное управление временем жизни (TTL) — уже из коробки.

Таблицы лидеров с отсортированными множествами
Отсортированные множества (Sorted Sets) незаслуженно игнорируются. А ведь они идеально подходят для всего, что связано с рейтингами.

// Add user score to leaderboard
await redis.zAdd("leaderboard:daily", {
  score: 9500,
  value: "user:123",
});
 
// Get top 10 players (with scores)
const top10 = await redis.zRangeWithScores(
  "leaderboard:daily",
  0,
  9,
  { REV: true }, // Descending order
);
 
// Get user's rank (0-indexed)
const rank = await redis.zRevRank("leaderboard:daily", "user:123");
 
// Get users around a specific user (5 above, 5 below)
const around = await redis.zRange("leaderboard:daily", rank - 5, rank + 5, {
  REV: true,
});

Это O(log n) для обновлений и O(log n + m) для запросов по диапазону.
Попробуйте добиться такой же производительности в Postgres — я подожду.

Распределённые блокировки (когда они действительно нужны)

Вот что меня подстерегло в начале: больше нельзя полагаться только на SETNX для распределённых блокировок. Это слишком наивный подход, и он обязательно подведёт вас со временем.

Правильное решение — это алгоритм Redlock,
но в большинстве случаев работает и более простой паттерн:

// Acquire lock with automatic expiration
async function acquireLock(
  lockKey: string,
  timeoutMs: number,
): Promise<string | null> {
  const lockId = crypto.randomUUID(); // Unique identifier for this lock holder
 
  const acquired = await redis.set(lockKey, lockId, {
    NX: true, // Only set if not exists
    PX: timeoutMs, // Expire after timeout (prevents deadlock)
  });
 
  return acquired ? lockId : null;
}
 
// Release lock (only if we own it)
async function releaseLock(lockKey: string, lockId: string): Promise<boolean> {
  // Lua script ensures atomic check-and-delete
  const script = `
    if redis.call("get", KEYS[1]) == ARGV[1] then
      return redis.call("del", KEYS[1])
    else
      return 0
    end
  `;
 
  const result = await redis.eval(script, {
    keys: [lockKey],
    arguments: [lockId],
  });
 
  return result === 1;
}
 
// Usage
const lockId = await acquireLock("invoice:generate:user123", 5000);
if (lockId) {
  try {
    // Do expensive operation that should only happen once
    await generateInvoice(userId);
  } finally {
    await releaseLock("invoice:generate:user123", lockId);
  }
} else {
  // Someone else is already processing this
}

Это идеально подходит для:

  • приложений такси (поиск ближайших водителей)

  • доставки еды (поиск ближайших ресторанов)

  • поиска магазинов (определение ближайших точек)

  • приложений недвижимости (объекты рядом с заданной точкой)

Альтернатива — использовать PostGIS или делать вычисления по формуле гаверсина в самом приложении. С Redis это становится тривиальной задачей.

Pub/Sub для функций в реальном времени


Pub/Sub отлично подходит, когда нужно разослать сообщения нескольким сервисам или WebSocket-подключениям.

// Subscriber (in your WebSocket server)
const subscriber = redis.duplicate();
await subscriber.subscribe("chat:room123", (message) => {
  broadcastToWebSockets(JSON.parse(message));
});
 
// Publisher (in your API server)
await redis.publish(
  "chat:room123",
  JSON.stringify({
    user: "charan",
    message: "Hello everyone!",
    timestamp: Date.now(),
  }),
);

Схема работы:

Pub/Sub работает по принципу "отправил и забыл" — если никто не слушает, сообщение исчезает.
Это не баг, а фича для задач с обновлениями в реальном времени.

Примеры использования:

  • уведомления в реальном времени

  • чаты

  • живые дашборды

  • рассылка через WebSocket

  • сигналы для инвалидции кэша

Streams для надёжных очередей сообщений
Когда вам нужен Pub/Sub, но с сохранением сообщений и гарантиями доставки — вам подойдут Streams.

// Add event to stream
await redis.xAdd("events:stream", "*", {
  type: "user_signup",
  userId: "123",
  timestamp: Date.now().toString(),
});
 
// Consumer group pattern (like Kafka consumer groups)
await redis.xGroupCreate("events:stream", "processors", "0", {
  MKSTREAM: true,
});
 
// Read and process messages
const messages = await redis.xReadGroup(
  "processors",
  "consumer1",
  { key: "events:stream", id: ">" },
  { COUNT: 10 },
);
 
messages.forEach(async ([stream, events]) => {
  for (const [id, fields] of events) {
    // Process event
    await processEvent(fields);
 
    // Acknowledge processing
    await redis.xAck("events:stream", "processors", id);
  }
});

Redis Streams не заменяют Kafka, но часто позволяют обойтись без неё в небольших системах (до миллиона сообщений в день).
Это компромисс, который стоит рассмотреть, прежде чем тащить Kafka в стек.

Надёжность и сохранность данных (здесь чаще всего ошибаются)


Популярный миф: «Redis теряет всё при перезапуске».
Это неправда. Redis поддерживает персистентность через:

  • RDB-снапшоты — периодические дампы состояния на диск. Быстро и компактно, но есть риск потерять несколько минут данных.

  • AOF (Append-Only File) — логирует каждую операцию записи. Более надёжно, но приводит к увеличению размера файлов и замедлению запуска.

Redis можно настроить под разные сценарии:

  • максимальная производительность — без персистентности

  • максимальная надёжность — AOF с fsync при каждой записи

  • баланс между ними — AOF с fsync раз в секунду (самый распространённый вариант)

Комментарий от Михаила Поливаха

Ещё AOF больше бьет по перформансу WRITE операций, но это так, к слову. И последнее, если вы вдруг решите делать fsync syscall на каждый WRITE, то я лично Вас найду, явлюсь к Вам ночью и мы детально обсудим, почему так делать почти никогда не надо

# In redis.conf
appendonly yes
appendfsync everysec  # Balance between performance and durability

Redis не предназначен для хранения критически важных данных в качестве основной базы.

Но и по умолчанию с данными он обращается вовсе не безответственно.
Понимайте компромиссы и настраивайте Redis соответственно.

Пайплайны и пакетные операции


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

// BAD: Makes 1000 round trips (~5 seconds)
for (let i = 0; i < 1000; i++) {
  await redis.set(`key:${i}`, `value:${i}`);
}
 
// GOOD: Batches into one round trip (~50ms)
const pipeline = redis.pipeline();
for (let i = 0; i < 1000; i++) {
  pipeline.set(`key:${i}`, `value:${i}`);
}
await pipeline.exec();

Даже если Redis обрабатывает каждую команду за микросекунды, сетевые обмены занимают миллисекунды.
Это быстро накапливается. Пайплайны позволяют превратить операцию, занимавшую 5 секунд, в 50 миллисекунд.

Разница — колоссальная.

Когда НЕ стоит использовать Redis


Будем честны насчёт ограничений. Redis даёт сбои, когда:

  • Нужны сложные реляционные запросы — если вам требуются JOIN-ы между несколькими таблицами с непростыми WHERE-условиями, используйте PostgreSQL. Не пытайтесь воссоздать SQL в Redis. Я видел, как это пытались сделать. Хорошо это не заканчивалось.

  • Данные не помещаются в память — Redis загружает всё в оперативную память. Если ваш датасет занимает 100 ГБ, а оперативной памяти у вас 16 ГБ — Redis просто не будет работать. Точка.

  • Требуется абсолютная надёжность хранения — несмотря на поддержку персистентности, в некоторых сценариях Redis может потерять данные. Для финансовых транзакций или критичных бизнес-данных, где потеря недопустима, используйте специализированные СУБД.

  • Полнотекстовый поиск и аналитика — у Redis есть модуль RediSearch, но для сложных полнотекстовых запросов и аналитики лучше подойдут Elasticsearch и подобные решения.

Использовать Redis как основную базу данных для бизнес-критичных данных — почти всегда ошибка.
А вот использовать его как специализированный компонент системы для конкретных задач — почти всегда отличное решение.

Архитектурный паттерн, который масштабируется


Вот ментальная модель, которая отлично себя зарекомендовала:

Redis — это общее, быстрое, хранимое в памяти состояние с атомарными операциями, на которые можно положиться.

Это не свалка для всего медленного. Это не способ избежать продумывания модели данных. Это инструмент, который вознаграждает за аккуратное проектирование и наказывает за неправильное использование.

Подумайте, с каким типом состояния вы работаете:

  • Производное состояние (кэши, счётчики, агрегаты) → Redis идеально подходит

  • Общее состояние (сессии, блокировки, лимиты запросов) → Redis идеально подходит

  • Упорядоченное состояние (очереди, таблицы лидеров, тайм-серии) → Redis идеально подходит

  • Состояние, привязанное к местоположению (геозапросы) → Redis идеально подходит

  • Базовые бизнес-данные (учётные записи, заказы, инвентарь) → PostgreSQL/MySQL

Архитектура становится гораздо понятнее, когда вы перестаёте рассматривать каждое хранилище данных как универсальный молоток.

Уроки после нескольких лет использования Redis в продакшене

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

  • Знайте свои структуры данных — уделите хотя бы один вечер, чтобы по-настоящему разобраться, в чём сильны разные структуры Redis. Эти знания приносят «сложный процент»: в момент, когда вы решаете задачу и вдруг осознаёте — «постойте, отсортированное множество идеально подойдёт!» — Redis раскрывает свою мощь.

  • Следите за использованием памяти — память — главный ограничивающий фактор в Redis. Используйте redis-cli --bigkeys, чтобы находить «прожорливые» ключи. Настройте maxmemory и maxmemory-policy под свою нагрузку. Мониторьте память в продакшене. Мне приходилось вставать в 3 утра из-за того, что кто-то забыл выставить TTL для кэш-ключей. Не будьте этим человеком.

  • Используйте пул соединений — каждая клиентская библиотека Redis поддерживает пул соединений. Пользуйтесь этим. Открытие TCP-соединений — дорогостоящая операция. Настраивайте размер пула под свою параллельную нагрузку (мы обычно используем 10–20 соединений на сервер приложения).

  • Думайте в терминах пайплайнов — если вы делаете несколько последовательных вызовов Redis, объединяйте их в пайплайн. Экономия на задержках — огромная, особенно если Redis не работает на localhost.

  • Не боритесь с архитектурой Redis — он оптимизирован под простые и быстрые операции. Если вы вдруг обнаружили, что пытаетесь делать сложные транзакции с несколькими ключами или реализовывать JOIN-ы, скорее всего, вы используете не тот инструмент.

  • Тестируйте сценарии отказа — сбой Redis не должен «ронять» всю систему. Убедитесь, что приложение умеет корректно деградировать. Мы усвоили этот урок на собственном опыте — сбой Redis вызвал каскадную аварию, которая повалила весь сайт.

Настоящий сдвиг в мышлении

Вот главная мысль, которую я хочу донести:

Перестаньте думать о Redis как о «кэше поверх базы данных».
Начните воспринимать его как сервер структур данных, который к тому же невероятно быстрый.

Когда вы проектируете систему и у вас появляется мысль:
«Мне нужна очередь», «Мне нужна таблица лидеров», «Нужно отслеживать активных пользователей»
не спешите сразу с решением «Построю в Postgres и потом закэширую в Redis».

Спросите себя:
«А может Redis сам может быть источником истины для этой задачи?»
Иногда ответ — да, и архитектура становится проще и быстрее.

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

Это фундаментальный строительный блок современных высокопроизводительных систем. И чем глубже вы его изучаете, тем большую ценность он приносит.

Попробуйте. Постройте что-то с отсортированными множествами, Pub/Sub или геопространственными индексами.

Вы удивитесь, насколько многое становится возможным, если перестать относиться к Redis как к «просто» чему-то.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.