Перейти к содержимому
Поиск
Разработка

Redis: кеш, TTL и координация — мастерская и Node.js

Прослеживаем путь данных между запросами: кеш, устаревшая цена, TTL, счётчики и аренда работы. Живая мастерская и настоящий проект Node.js + Redis для Windows и Linux VPS.

Александр Некрасов
Redis — кеш, время жизни ключей и быстрые данные

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

В этой статье пройдём восемь связанных сценариев: cache-aside, TTL, устаревшие данные и инвалидация, кеш отсутствующего результата, атомарный лимит, аренда с токеном, объединение параллельных чтений и работа без кеша. На странице можно поиграть с моделью, а в архиве находится настоящий Redis 8.10.3 с Node.js API, тестами и инструкциями для Windows, Linux и VPS.

Что такое Redis и где он стоит в приложении

Redis — отдельный сервер хранения данных с быстрыми командами над строками и другими структурами. Приложение подключается к нему через клиентскую библиотеку. Браузер обычно общается с вашим HTTP API, а уже API решает, какие значения прочитать или изменить. Доступ к самому Redis посетителю сайта не выдаётся.

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

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

Когда Redis полезен, а когда добавляет лишнюю сложность

Кеш имеет смысл, когда повторных чтений заметно больше, чем изменений, а получение ответа стоит дороже, чем доступ к сохранённой копии. Для маленького внутреннего списка с несколькими запросами в минуту дополнительный сервер может оказаться ненужным. Сначала полезно измерить медленное место: запрос к базе, внешнее API, вычисление или сборку ответа.

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

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

Мини-мастерская: проследите путь данных

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

На странице статьи работает явно обозначенная браузерная модель. Она воспроизводит основные операции в памяти вкладки и не подключается к серверному Redis. В архиве тот же интерфейс использует настоящий Node.js API и Redis. Режим определяется явно: ошибка серверной сессии не переключает работающий стенд незаметно в локальную модель.

Источник данных в обоих режимах учебный: массив товаров, а не SQL-база. Чтение намеренно задержано на 35 миллисекунд, чтобы показать повторную работу. Это не тест производительности Redis против конкретной СУБД. Снимок TTL обновляется после действия или по кнопке, поэтому показанное число не выдаётся за непрерывный таймер.

Мастерская прокручивается внутри статьи. На телефоне можно открыть её отдельно.

Первое подключение Node.js: ошибки тоже часть контракта

В проекте используется node-redis 6.3.1. Клиент создаётся один раз на процесс, получает обработчик события error и устанавливает соединение. Пароль читается из локального файла секрета. Он не хранится в JavaScript, отправляемом браузеру, и не передаётся пользователю вместе с конфигурацией интерфейса.

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

Ниже сокращённое подключение для своего приложения. Полный вариант находится в src/client.mjs. В локальном Compose имя redis доступно внутри сети контейнеров. Оно не является адресом, который нужно открывать из браузера или использовать в другом сервере без сетевой настройки.

import { createClient } from 'redis';
import { readFile } from 'node:fs/promises';

const client = createClient({
  socket: { host: 'redis', connectTimeout: 750 },
  password: (await readFile('/run/secrets/redis-password', 'utf8')).trim(),
  disableOfflineQueue: true,
  commandOptions: { timeout: 500 },
});
client.on('error', () => { /* журнал без пароля и пользовательских данных */ });
await client.connect();

Документация: node-redis: установка и подключение.

Cache-aside: MISS, источник, запись, затем HIT

Начните с первого сценария и дважды прочитайте один товар. При первом запросе ключ отсутствует: это MISS. Приложение обращается к источнику, сериализует результат и сохраняет его с ограниченным временем жизни. Следующее чтение получает HIT и использует подготовленную копию. Счётчик источника при этом не увеличивается.

Обёртка JSON содержит поле product. Она понадобится и для отсутствующего товара: строка с product: null отличается от отсутствующего ключа. Сам факт наличия строки в Redis не означает, что данные подходят текущей версии приложения. Для изменяющегося формата полезно включать версию схемы в ключ или значение и уметь отбросить старую запись.

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

const key = 'catalog:v1:product:1';
const cached = await client.get(key);
if (cached !== null) return JSON.parse(cached);

const product = await repository.findById(1);
const value = { product };
await client.set(key, JSON.stringify(value), { PX: 10000 });
return value;

TTL: срок жизни копии, а не срок жизни товара

TTL определяет, сколько может существовать запись в Redis. Истечение удаляет копию, но не меняет основной источник. В мастерской выберите три секунды, запишите товар, подождите и прочитайте снова. После истечения приложение получит MISS и снова обратится к каталогу.

Изменение настройки TTL применяется к следующей записи. Оно не переписывает время существующего ключа автоматически. Это полезная деталь для интерфейса настроек: человек должен понимать, меняет ли он политику будущих записей или уже накопленные данные. Для второго случая нужна отдельная команда и понятная область действия.

PTTL возвращает миллисекунды, а TTL — секунды. Значение -2 означает отсутствие ключа, -1 — наличие без срока жизни. В нашем проекте все временные ключи получают срок. Если вы видите неожиданное -1, проверьте код записи: отдельный SET может заменить значение и потерять прежний TTL, если сохранение срока не было явно предусмотрено.

SET lesson:message "hello" EX 30
GET lesson:message
TTL lesson:message
PTTL lesson:message
DEL lesson:message

Документация: TTL и специальные значения.

Документация: SET с EX, PX и условиями записи.

Устаревшая цена: инвалидация должна быть частью записи

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

Кнопка удаления копии выполняет инвалидацию. Следующее чтение получит новую цену и сохранит актуальный результат. В прикладном коде удаление ключа обычно связывают с успешным изменением источника. Если транзакция базы откатилась, публиковать новый результат или считать изменение завершённым нельзя.

Простой порядок «обновить базу, удалить кеш» имеет гонки при параллельных запросах: старое чтение может завершиться позже и снова положить устаревшее значение. В лаборатории действия одной сессии выполняются последовательно. Для строгих требований в реальном продукте рассматривают версии данных, события после транзакции и другие механизмы согласования, а не обещают абсолютную свежесть одним DEL.

await repository.updatePrice(productId, newPrice);
await client.del(`catalog:v1:product:${productId}`);
// При конкурентных чтениях отдельно решите вопрос версии и порядка событий.

Negative cache: не искать отсутствующий товар снова и снова

Отсутствующий результат тоже может повторяться. Например, по старой ссылке постоянно запрашивают удалённый товар. Если каждый такой запрос идёт в основную базу, кеш положительных карточек не решит проблему. Короткая запись об отсутствии позволяет временно отвечать без повторного чтения.

В мастерской используется фиксированный ID 999. Первый запрос создаёт запись с product: null, второй получает HIT, но результат по-прежнему означает «товар не найден». Срок такой записи — три секунды, меньше обычной карточки. Если объект вскоре появится, слишком долгий отрицательный кеш не должен скрывать его надолго.

Не разрешайте посетителю бесконечно создавать ключи для произвольных идентификаторов. В нашем API допустимые товары ограничены, а отсутствующий ID задан отдельным действием. Для открытого каталога потребуется ограничение пространства запросов, частоты и размера кеша. Иначе попытка защитить базу превратится в неограниченный рост числа ключей.

const product = await repository.findById(id);
await client.set(`catalog:v1:product:${id}`,
  JSON.stringify({ product }), { PX: product ? 10000 : 3000 });

Структура ключей и изоляция пользователей

Ключ — часть контракта между компонентами приложения. Удобно выделять назначение, версию и идентификатор: catalog:v1:product:42. Если данные различаются по организации, её идентификатор тоже должен присутствовать. Избыточно длинные ключи и случайные пользовательские строки усложняют обслуживание и могут расходовать память.

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

Кнопка сброса удаляет только заранее известные ключи своей сессии. Здесь нет FLUSHALL, массового удаления чужих данных или исполнения команды, введённой в текстовое поле. Для обычного сервиса такой принцип также полезен: административная операция должна иметь явную границу, которую можно проверить до выполнения.

const prefix = `catalog:v1:tenant:${tenantId}:`;
const key = `${prefix}product:${productId}`;
// tenantId и productId получены из проверенного контекста, а не готового ключа клиента.

Атомарный счётчик: зачем одного INCR недостаточно

INCR удобно увеличивает числовое значение. Но ограничение запросов требует ещё и срока окна. Если выполнить INCR и PEXPIRE двумя отдельными командами, процесс может оборваться между ними. Тогда счётчик останется без ожидаемого срока. Для этой маленькой связанной операции в примере используется Lua.

Скрипт увеличивает значение и устанавливает десятисекундный срок только при первой попытке. Затем возвращает счётчик и оставшееся время. Redis выполняет этот скрипт как одну операцию относительно других команд. В тесте двенадцать параллельных вызовов получают уникальную последовательность чисел, а ключ имеет TTL.

В мастерской разрешены первые три попытки, четвёртая получает отказ. Клиенты A и B имеют отдельные ключи. Это учебные обозначения, а не авторизация пользователей. В реальном API идентичность для лимита должна формироваться из доверенного контекста, иначе посетитель сможет обходить правило сменой произвольного параметра.

const script = `
local n = redis.call('INCR', KEYS[1])
if n == 1 then redis.call('PEXPIRE', KEYS[1], 10000) end
return {n, redis.call('PTTL', KEYS[1])}
`;
const [count, remainingMs] = await client.eval(script, {
  keys: ['rate:demo:A'], arguments: [],
});
const allowed = count <= 3;

Документация: INCR и варианты счётчиков.

Фиксированное окно и границы ограничения частоты

Наше окно начинается с первой попытки и живёт десять секунд. Это простая модель фиксированного окна, а не скользящий интервал. Вблизи границы можно получить несколько разрешённых запросов подряд: последние попытки старого окна и первые нового. Для некоторых внутренних действий это допустимо, для других нужен иной алгоритм.

Лимитирование также не заменяет права доступа. Разрешение трёх запросов не доказывает, что пользователь вообще имеет право выполнить действие. Авторизация проверяется отдельно. При сбое Redis в примере лимит не превращается автоматически в успешную проверку: интерфейс сообщает, что координационная операция не выполнена.

После перезапуска нашего cache-only Redis счётчики исчезают. Поэтому этот стенд объясняет атомарность и поведение окна, но не обещает устойчивую защиту от злоупотребления при любых отказах. Для реальной системы решение о допустимом поведении во время аварии должно быть согласовано с назначением конкретного endpoint.

Аренда работы: SET NX PX и токен владельца

Иногда нужно не дать двум исполнителям одновременно начать одну небольшую операцию. В лаборатории клиент A пытается создать ключ аренды со случайным токеном и сроком пять секунд. Условие NX разрешает запись только при отсутствии ключа. Если A успел, B получает отказ и не начинает работу.

Освобождать аренду простым DEL нельзя. Пока старый исполнитель работал, его срок мог закончиться, а другой клиент уже получил новую аренду. Старый DEL удалил бы чужое право работы. Поэтому Lua сравнивает сохранённый токен со своим и удаляет ключ только при совпадении.

Попробуйте A: получить, B: получить, B: освободить. Последнее действие не удаляет аренду A. После истечения срока B может получить её, а старый токен A снова не должен влиять на чужой ключ. Это понятный пример владения, который можно проверить по реальным командам и результатам.

const token = crypto.randomUUID();
const acquired = await client.set('lock:report', token, { NX: true, PX: 5000 });
if (!acquired) throw new Error('Работа уже выполняется');

try { await shortJob(); } finally {
  await client.eval(
    "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end",
    { keys: ['lock:report'], arguments: [token] },
  );
}

Документация: владение и ограничения блокировок.

Почему аренда не обещает exactly-once

Истечение TTL не останавливает старый процесс. Если операция затянулась дольше аренды, новый исполнитель может начать работу, пока старый ещё выполняет внешний запрос или запись. Токен защищает освобождение ключа, но сам по себе не делает вашу базу или внешнюю систему участником одной транзакции.

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

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

Пять запросов одновременно: объединяем ожидание

Если популярный ключ истёк, несколько запросов могут одновременно получить MISS и все пойти в источник. Такая волна особенно неприятна для дорогого вычисления. Сценарий пяти чтений показывает простой способ уменьшить повторную работу внутри одного процесса: сохранить обещание уже запущенного получения данных и вернуть его остальным ожидающим.

В мастерской перед опытом копия выбранного товара удаляется. Пять чтений стартуют вместе, одно обращается к источнику, четыре присоединяются к ожиданию. Счётчики показывают фактическое число обращений и объединений. Это отдельный механизм от TTL: он не делает запись вечной и не меняет её свежесть.

Map с выполняющимися запросами существует только в одном Node.js-процессе. При нескольких экземплярах приложения каждый может запустить своё получение. Для общей координации потребуется другая схема, а для некоторых задач достаточно допустить несколько одинаковых вычислений. Обязательно удаляйте обещание после ошибки, иначе неудачный запрос может надолго закрепиться как общий результат.

const inFlight = new Map();
async function once(key, loader) {
  if (inFlight.has(key)) return inFlight.get(key);
  const pending = loader();
  inFlight.set(key, pending);
  try { return await pending; }
  finally { inFlight.delete(key); }
}

Отказ кеша: источник остаётся, координация не притворяется успешной

Включите обход кеша и прочитайте товар. Интерфейс покажет FALLBACK: результат получен из источника. Этот режим намеренно имитирует ошибку доступа на уровне приложения. В архиве также проверен настоящий отказ: контейнер Redis останавливался, клиент терял соединение, чтение продолжалось через источник, а после запуска первая попытка снова получала MISS.

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

Попытка лимита или получения аренды при недоступном Redis возвращает отказ. Приложение не может утверждать, что проверка прошла, если она не была выполнена. Баннер настоящего стенда отдельно показывает доступность соединения, а результат действия объясняет, откуда пришли данные и какая операция не состоялась.

let cached;
try { cached = await client.get(key); }
catch { return repository.findById(id); }
// Такая ветка предназначена для необязательного кеша чтения.
// Для лимита или аренды результатом ошибки будет отказ операции.

Память: maxmemory и политика вытеснения

Redis хранит рабочие данные в памяти, поэтому размер должен быть ограничен. В нашем стенде maxmemory равен 64 МБ, а контейнер имеет отдельный предел 128 МБ с запасом на накладные расходы. Значения выбираются для небольшой лаборатории, а не как универсальная настройка любого production-сервера.

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

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

maxmemory 64mb
maxmemory-policy noeviction

Документация: политики вытеснения ключей.

Сохранение на диск: кеш и основная база — разные задачи

В учебном Compose отключены RDB-снимки и AOF. Это намеренный cache-only режим: после перезапуска копии восстанавливаются из источника. Заодно исчезают демонстрационные лимиты и аренды. После перезапуска Node.js сбрасывается и учебный массив цен, потому что он тоже хранится в памяти процесса.

Такое поведение нельзя переносить на единственную копию заказов или документов. Если Redis используется для состояния, которое требуется восстанавливать, отдельно выбирают persistence, том, периодичность, допустимую потерю данных и способ проверки резервной копии. Само наличие файла на диске ещё не доказывает, что восстановление приложения работает.

В нашей лаборатории резервируются код и закрытая конфигурация; исходная база реального продукта должна иметь собственные резервные копии. Для очереди задач требования тоже будут отличаться. Это полезно сопоставить со статьёй о BullMQ, где Redis участвует в другом жизненном цикле данных.

save ""
appendonly no

Документация: RDB, AOF и восстановление.

Windows: запускаем настоящий стенд в Docker

Для Windows используется Docker Desktop с Linux containers. Архив распаковывается в отдельную папку. Генератор секретов можно выполнить в одноразовом официальном контейнере Node.js, поэтому устанавливать Redis непосредственно в Windows не требуется. Случайный пароль создаётся локально и не выводится в терминал.

После запуска Compose откройте 127.0.0.1:8115. Именно этот адрес указан в разрешённом Host. Баннер должен сообщить о настоящем Node.js API и доступном Redis. Затем дважды прочитайте товар и проверьте MISS/HIT. Команда интеграционных тестов выполняется внутри приложения, где доступно имя redis из закрытой сети.

Если локально установлен Node.js 24, можно также установить зависимости и запустить тесты модели. Но обычный npm start на хосте не получает автоматически доступ к Redis внутри закрытой сети Compose. Полный комплект запускается контейнерами; смешанный режим требует отдельно настроенного подключения и не нужен для первого знакомства.

cd C:\Projects\redis-project
docker run --rm -v "${PWD}:/app" -w /app node:24.22.2-bookworm-slim node scripts/init.mjs
docker compose up -d --build
docker compose ps
docker compose exec app npm run test:integration

Linux: одинаковый проект на рабочей машине и сервере

На Linux установите Docker Engine и Compose plugin по официальной инструкции для своего дистрибутива. Для Ubuntu ссылка приведена ниже и в README. Генератор запускается с UID и GID текущего пользователя, чтобы созданная папка секретов принадлежала оператору, а не случайно оказалась недоступной после выполнения через sudo.

У приложения две сети: front обеспечивает опубликованный loopback-порт, cache соединяет его с Redis и отмечена internal. Сам Redis не публикует 6379 на хост. Такое разделение сохраняет понятный вход через HTTP и не превращает базу команд в публичный сервис.

Сборка приложения выполняет тесты модели. После старта отдельно запускаются проверки настоящего Redis и HTTP. При ошибке сети Docker не используйте глобальный prune без понимания чужих проектов. На перегруженном тестовом хосте можно согласовать отдельную подсеть локальным override; это свойство окружения, а не обязательная часть приложения.

cd ~/redis-project
sudo docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/app" -w /app node:24.22.2-bookworm-slim node scripts/init.mjs
sudo docker compose up -d --build
sudo docker compose exec app npm run test:integration
curl -fsS http://127.0.0.1:8115/health

Документация: Docker Engine на Ubuntu.

Linux VPS: HTTPS, доступ сотрудника и секреты

Для публикации стенда на выделенном сервере в README дан путь с отдельным доменом и Caddy. Приложение остаётся на 127.0.0.1:8115, а снаружи доступны только 80/443. Caddy выпускает сертификат при корректных DNS и сетевых условиях и запрашивает пароль сотрудника через basic_auth. Если на сервере уже настроен Nginx, используйте его для такого же входа вместо второго фронтового сервера.

PUBLIC_ORIGIN должен совпадать с внешним HTTPS-адресом. Он определяет допустимые Host и Origin, а не включает шифрование самостоятельно. Файлы Redis-пароля и конфигурации лежат в закрытой папке secrets, исключённой из образа и публичного архива. Повторный генератор не меняет действующий пароль, а проверяет согласованность пары файлов.

После настройки сначала проверьте отказ без пароля, затем запрос health с паролем, затем работу мастерской в браузере. Cookie получает Secure при HTTPS, а действия требуют CSRF-токен. Эти меры соответствуют закрытому учебному инструменту. Для полноценного сервиса нужны реальные учётные записи и права, а кнопки A/B такими учётными записями не являются.

cache-lab.example.com {
    import /etc/caddy/redis-lab-auth.caddy
    request_body {
        max_size 2KB
    }
    reverse_proxy 127.0.0.1:8115
}
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
curl -I https://cache-lab.example.com/
curl -u editor https://cache-lab.example.com/health

Документация: сетевая модель безопасности Redis.

Консоль, диагностика и проверка отказа

Для повседневной проверки полезны PING, INFO memory и INFO stats. Они помогают увидеть доступность, расход памяти и общие счётчики. Команды из приложения в журнале мастерской показаны в пределах конкретной сессии. Не нужно включать MONITOR на общем сервере только ради первого знакомства: он раскрывает поток команд и создаёт дополнительную нагрузку.

В README есть команда входа в redis-cli внутри контейнера без печати пароля. Это административная консоль доверенного оператора, а не функция пользовательского интерфейса. Для production-приложения с общим Redis полезен отдельный ACL-пользователь с разрешёнными командами и префиксами; наша конфигурация содержит один изолированный учебный экземпляр.

Проверка отказа особенно полезна до первой эксплуатации. Остановите только сервис redis текущего Compose-проекта, прочитайте товар и попробуйте лимит. Затем запустите Redis обратно и дождитесь переподключения. Так становится видно, соответствует ли реальная деградация тому, что обещает интерфейс, а не только удачному сценарию разработки.

docker compose stop redis
curl -fsS http://127.0.0.1:8115/health
# В браузере: чтение FALLBACK, лимит не выполнен.
docker compose start redis
docker compose logs --tail=100 app redis

Обновление и откат без потери границ данных

Новый релиз распаковывается в отдельную папку. Закрытая конфигурация переносится согласованной парой, затем собирается образ и выполняются проверки. Интеграционные тесты предназначены только для своего учебного экземпляра: среди них есть временное изменение maxmemory. Их нельзя направлять на сторонний production Redis, где работают другие приложения.

После обновления проверьте health, два чтения MISS/HIT, отдельную браузерную сессию и поведение после истечения TTL. Старую папку и образ сохраняйте до завершения проверки. Для отката запустите предыдущий релиз с прежней конфигурацией и снова проверьте прикладной сценарий, а не только наличие процесса.

В этой лаборатории потеря кеша ожидаема. В настоящем продукте обновление может затронуть формат значений, префиксы, события и основной источник. Версия схемы ключа позволяет постепенно отделять новые копии от старых. Но смена префикса увеличивает временный объём памяти, поэтому даже такое простое решение требует учёта оставшихся ключей и их TTL.

Что проверено и что взять в свой проект

Тесты модели используют управляемое время и проверяют истечение TTL, отрицательный кеш, устаревшую цену, владение арендой, ограничение окна и объединение запросов. Отдельный набор работает с настоящим Redis: двенадцать параллельных INCR, Lua-освобождение, OOM, изоляция двух HTTP-сессий, CSRF, Host, Origin и запрет выдачи закрытых файлов.

Также проверялась реальная остановка Redis. Чтение вернулось к источнику, координация отказала, после перезапуска появился MISS. Это отличает работающий пример отказа от переключателя, который просто меняет надпись в интерфейсе. На странице статьи переключатель честно обозначен как модель, а серверный вариант воспроизводится из архива.

В собственное приложение стоит перенести не только GET и SET, но и договорённости: что является источником истины, какая свежесть допустима, кто удаляет копию, как ограничены ключи и какое поведение ожидается при отказе. Именно эти решения превращают Redis из дополнительного контейнера в полезную часть сервиса.

Исходники и официальные материалы

В архиве находятся интерфейс, общая логика сценариев, браузерный адаптер, настоящий Redis-адаптер, Node.js API, генератор локальных секретов, тесты и Compose. Подписка или облачный API-ключ не нужны. Redis скачивается как отдельный официальный образ, а не упакован внутрь исходников мастерской.

Собственный код распространяется под MIT. У Redis Server выбранной версии собственные варианты лицензирования; ссылки на них, а также лицензии node-redis и Bootstrap перечислены в THIRD-PARTY-LICENSES.txt. Для дальнейшей работы используйте документацию именно тех компонентов и версий, которые разворачиваете, особенно при изменении persistence, репликации и политики памяти.