Redis в продакшене: кэширование, очереди, блокировки и отказоустойчивость

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

Redis начинается как «быстрый кэш», а через год в продакшене оказывается очередью задач, хранилищем сессий, счётчиком ограничения запросов, распределённой блокировкой и шиной событий одновременно. Это сила Redis — и главный источник проблем: данные, потеря которых была допустима для кэша, внезапно становятся критичными, а одна инсталляция без репликации и мониторинга — единой точкой отказа всего продукта. Ниже — практическое руководство по Redis для продакшена: структуры данных и задачи, которые они решают, паттерны кэширования и инвалидации, очереди и события, блокировки и ограничение частоты, устойчивость данных и высокая доступность, типичные ошибки и мониторинг. Отдельно — смена лицензии Redis и появление Valkey, которые важно учитывать при выборе решения. Что такое Redis и когда он нужен Redis — хранилище данных в оперативной памяти с богатым набором структур и возможностью сохранять данные на диск. Операции выполняются за доли миллисекунды, поэтому Redis используют там, где основная база данных слишком медленна или неудобна. кэширование результатов запросов и вычислений; сессии и временные токены; ограничение частоты запросов; очереди фоновых задач; рейтинги и счётчики в реальном времени; распределённые блокировки; обмен событиями между сервисами; хранение состояния для горизонтально масштабируемых сервисов — например, комнат сервера WebSocket. Redis не стоит использовать как основную базу данных для данных, потеря которых недопустима, если это не спроектировано осознанно: память дороже диска, а гарантии сохранности зависят от настроек и архитектуры. Лицензия Redis и Valkey В 2024 году Redis сменил лицензию с открытой BSD на лицензии с ограничениями для поставщиков управляемых сервисов. В ответ под эгидой Linux Foundation появился форк Valkey, сохранивший открытую лицензию и совместимость с протоколом и командами Redis; крупные облачные провайдеры начали поддерживать его. Позже Redis добавил вариант лицензирования под AGPL. Для большинства компаний, которые используют Redis внутри своего продукта, смена лицензии мало что меняет на практике. Но при выборе управляемого сервиса и долгосрочной стратегии стоит проверить, какая именно реализация используется — Redis или Valkey — и совместимы ли нужные возможности. Клиентские библиотеки и базовые команды для обоих в основном одинаковы, и всё сказанное ниже применимо к обоим. Структуры данных и их применение Строки Базовый тип: текст, числа, сериализованные объекты. Поддерживают атомарный инкремент и время жизни. SET session:ab12 "{\"userId\":42}" EX 3600 # значение со сроком жизни 1 час INCR page:views:2026-09-13 # атомарный счётчик SET lock:invoice:77 worker-3 NX PX 30000 # установить, только если ключа нет Хеши Объект с полями. Позволяют читать и менять отдельные поля без передачи всего объекта. HSET user:42 name "Анна" plan "pro" HINCRBY user:42 credits -1 Списки и потоки Списки подходят для простых очередей. Потоки (Streams) — для надёжной обработки событий: сообщения сохраняются, у каждого есть идентификатор, группы потребителей распределяют обработку и отслеживают подтверждения. XADD orders:events * type created orderId 1001 XREADGROUP GROUP billing worker-1 COUNT 10 BLOCK 5000 STREAMS orders:events XACK orders:events billing 1726212345678-0 Множества и упорядоченные множества Множества хранят уникальные значения — теги, участников, просмотренные элементы. Упорядоченные множества хранят значения с весом и позволяют получать диапазоны по весу: рейтинги, очереди с приоритетом, скользящие окна по времени. ZINCRBY leaderboard 15 "user:42" ZREVRANGE leaderboard 0 9 WITHSCORES # десять лучших Другие структуры HyperLogLog приблизительно считает уникальные значения при фиксированном небольшом объёме памяти, битовые массивы — хранят флаги для большого числа идентификаторов, геоиндексы — ищут объекты в радиусе. Кэширование Cache-aside Самый распространённый паттерн: приложение сначала проверяет кэш, при промахе читает из базы данных и сохраняет результат. async function getProduct(id) { const key = `product:${id}`; const cached = await redis.get(key); if (cached) return JSON.parse(cached); const product = await db.products.findById(id); if (product) { await redis.set(key, JSON.stringify(product), { EX: 600 }); } return product; } async function updateProduct(id, patch) { const product = await db.products.update(id, patch); await redis.del(`product:${id}`); // инвалидация после записи return product; } Инвалидация Самая сложная часть кэширования. Практичные правила: у каждого ключа есть время жизни — даже если инвалидация при изменении настроена, это страховка от забытых мест; при изменении данных ключ удаляют, а не перезаписывают новым значением: так меньше риск, что параллельный запрос запишет в кэш устаревшие данные; версионирование ключей — catalog:v7:... — позволяет сбросить целую группу кэша сменой версии; кэшируются данные, для которых известно, насколько устаревание допустимо: остатки на складе и цена в корзине требуют иной политики, чем описание товара. Защита от лавины промахов Когда популярный ключ истекает, сотни одновременных запросов идут в базу данных за одним и тем же значением. Решения: небольшой случайный разброс времени жизни, блокировка на пересчёт значения, при которой только один запрос обращается к базе, а остальные ждут или получают предыдущее значение, и заблаговременное обновление популярных ключей до истечения. Очереди и фоновые задачи Для фоновых задач — отправки писем, генерации документов, обработки изображений — обычно используют готовые библиотеки поверх Redis, например BullMQ для Node.js: они реализуют повторные попытки, отложенные задачи, приоритеты, ограничение параллельности и отслеживание неудачных задач. import { Queue, Worker } from 'bullmq'; const emails = new Queue('emails', { connection }); await emails.add('welcome', { userId: 42 }, { attempts: 5, backoff: { type: 'exponential', delay: 2000 } }); new Worker('emails', async (job) = { await sendWelcomeEmail(job.data.userId); }, { connection, concurrency: 10 }); Важно: задачи должны быть идемпотентными — повторное выполнение не должно отправлять письмо дважды или повторно списывать деньги. Pub/Sub или Streams Pub/Sub доставляет сообщение только подписчикам, подключённым в момент публикации: если подписчик отключён, сообщение потеряно. Он подходит для уведомлений в реальном времени, где потеря допустима. Для событий, которые обязательно должны быть обработаны, используют Streams с группами потребителей или полноценный брокер сообщений. Подробно о событийной архитектуре — в статье об event-driven архитектуре . Распределённые блокировки Блокировка гарантирует, что операцию одновременно выполняет только один экземпляр приложения: например, ночной расчёт или обработку конкретного счёта. const token = crypto.randomUUID(); const acquired = await redis.set(`lock:invoice:${id}`, token, { NX: true, PX: 30_000 }); if (!acquired) return; // кто-то уже обрабатывает try { await processInvoice(id); } finally { // Удаляем, только если блокировка всё ещё наша await redis.eval( "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", { keys: [`lock:invoice:${id}`], arguments: [token] }, ); } Ограничения нужно понимать: если операция длится дольше времени жизни блокировки, другой процесс может её получить. Для операций, где двойное выполнение недопустимо, блокировку дополняют проверкой на уровне базы данных — уникальными ограничениями или версиями записей. Ограничение частоты запросов async function allow(userId, limit = 100, windowSec = 60) { const key = `rl:${userId}:${Math.floor(Date.now() / 1000 / windowSec)}`; const count = await redis.incr(key); if (count === 1) await redis.expire(key, windowSec); return count = limit; } Фиксированное окно просто, но допускает всплеск запросов на границе окон. Скользящее окно на упорядоченном множестве или алгоритм «ведра токенов» распределяют нагрузку равномернее. Где и какие лимиты ставить в API, — в статье о безопасности API . Сохранность данных RDB — периодические снимки на диск. Быстрое восстановление, но данные после последнего снимка теряются. AOF — журнал операций. Меньше потерь в зависимости от частоты синхронизации с диском, больше нагрузка на запись. Комбинация RDB и AOF — частый выбор, когда данные важны. Без сохранения — для чистого кэша, который можно восстановить из основной базы. Решение принимается для каждого использования отдельно. Кэш и очередь платежей в одной инсталляции с одинаковыми настройками — частая ошибка; разные по критичности данные лучше разносить по разным инсталляциям. Высокая доступность и масштабирование Репликация — копии основного узла для отказоустойчивости и распределения чтения. Репликация асинхронная: при отказе основного узла последние записи могут быть потеряны. Sentinel — отслеживает узлы и автоматически переключает на реплику при отказе основного. Cluster — распределяет данные по нескольким основным узлам с шардированием по слотам ключей. Операции над несколькими ключами работают, только если ключи находятся в одном слоте — это достигается общими частями имени в фигурных скобках, например {user:42}:cart и {user:42}:profile . Управляемые сервисы облачных провайдеров берут на себя репликацию, переключение и резервное копирование. Российские облачные провайдеры предлагают управляемые Redis-совместимые сервисы, что важно для размещения данных в России. Типичные ошибки Ключи без времени жизни — память растёт, пока не закончится. Команда KEYS в продакшене блокирует сервер на время перебора всех ключей. Используйте SCAN . Большие ключи — огромные списки, хеши и строки мегабайтного размера замедляют операции, репликацию и удаление. Не настроена политика вытеснения — при нехватке памяти Redis начинает отклонять запись или вытесняет не те данные. Для кэша обычно подходит вытеснение давно не использованных ключей, для хранилища важных данных — отказ записи и оповещение. Redis без пароля и доступный из сети — открытые инсталляции регулярно компрометируются. Доступ только из внутренней сети, аутентификация, TLS. Кэш как источник истины — данные существуют только в Redis без сохранения и репликации. Одна инсталляция на всё — кэш, сессии, очереди и блокировки с общими настройками и общей точкой отказа. Мониторинг использование памяти и число вытесненных ключей; доля попаданий в кэш — промахи показывают, работает ли кэширование; задержка команд и медленные команды из журнала SLOWLOG ; число подключений и отклонённых подключений; отставание реплик; длина очередей и потоков, число необработанных сообщений. Как построить наблюдаемость инфраструктуры и приложений, — в статье о мониторинге и observability . Архитектуру кэширования, очередей и высоконагруженных сервисов мы проектируем в рамках разработки веб-проектов . Когда нагрузка превышает возможности кэширования и одной базы данных, пригодится статья о шардировании . Частые вопросы Redis или Memcached для кэша? Для простого кэша строк оба подходят. Redis выбирают чаще благодаря структурам данных, времени жизни на уровне ключей, репликации и возможности использовать его заодно для сессий, очередей и ограничения частоты. Можно ли потерять данные в Redis? Да, в зависимости от настроек: без сохранения на диск — при перезапуске, с периодическими снимками — данные после последнего снимка, при асинхронной репликации — последние записи при отказе основного узла. Критичные данные должны иметь источник истины в надёжной базе данных. Redis или Valkey? Для большинства сценариев они совместимы на уровне команд и клиентских библиотек. Выбор определяется лицензионными требованиями компании, доступностью управляемого сервиса у выбранного провайдера и нужными расширенными возможностями конкретной реализации. Нужен ли Redis Cluster? Когда данные или нагрузка не помещаются в один узел. Для многих продуктов достаточно основного узла с репликами и автоматическим переключением. Cluster добавляет ограничения на операции с несколькими ключами, которые нужно учитывать в коде. Как выбрать время жизни кэша? Исходя из тог