Срок жизни кеша
Бессрочная копия каталога может бесконечно возвращать старый остаток после изменения основных данных. Добавим TTL — срок жизни ключа кеша. Через тридцать секунд сохранённая строка перестанет быть доступной, и следующий запрос снова прочитает основной hash. Так у учебной копии появится определённая временная граница, не затрагивающая сами товары.
TTL сокращает время возможной устарелости, но не гарантирует немедленного обновления после записи. В этом уроке изучаем именно срок жизни; инвалидацию по изменению добавим следующим состоянием. Используйте stages/02, где изменяется только политика SET кеша. Пример not_run: ожидание тридцати секунд и обращения к Redis при подготовке не выполнялись, поэтому результат ниже ожидаемый.
Значение и срок записываются вместе
Команда SET поддерживает опцию EX, задающую срок в секундах. Передаём её тем же вызовом, которым сохраняем JSON. Если сначала выполнить SET, а затем отдельный EXPIRE, между ними может произойти остановка процесса. Ключ тогда останется бессрочным. Один вызов фиксирует значение вместе с его сроком и устраняет именно эту промежуточную ситуацию; он не делает все операции кеширования единой транзакцией.
Полный файл cache.js второго снимка отличается от предыдущего добавленными аргументами 'EX', 30. Основной каталог читается прежней функцией, а ключ всё ещё использует постоянное поколение 0.
import { readCatalog, generationKey } from './catalog.js';
export async function cachedCatalog(redis) {
const generation = '0';
const key = `importer:cache:catalog:${generation}`;
const cached = await redis.get(key);
if (cached) return { source: 'cache', items: JSON.parse(cached) };
const items = await readCatalog(redis);
await redis.set(key, JSON.stringify(items), 'EX', 30);
return { source: 'primary', items };
}
При промахе начинается новый тридцатисекундный период. Обычный GET не продлевает его. Поэтому частые чтения не делают копию вечной: после истечения следующий запрос должен снова наполнить кеш. Это фиксированный срок после заполнения. Если приложение продлевало бы TTL на каждом обращении, популярная старая копия могла бы жить очень долго, и граница актуальности стала бы зависеть от активности пользователей.
Число 30 относится к примеру и не получено из измерений. Для настоящего каталога подходящий срок зависит от допустимой устарелости, частоты изменения, стоимости чтения источника и количества запросов. Остаток товара, подсказка поиска и справочная информация могут иметь разные требования. Нельзя выбрать одно удобное число и считать его правильным для всех данных, которые хранятся в Redis.
Истечение относится только к копии
Основной hash importer:products не получает TTL. После удаления cache-ключа товары остаются доступными для повторного чтения. Если бы срок поставили на hash с остатками, истечение означало бы потерю данных, а fixture затем восстановил бы начальный каталог. Такой результат был бы совсем другой операцией, поэтому полезно ещё раз проследить, какой ключ передаётся каждой команде и почему именно он считается заменяемой копией.
Ожидаемый сценарий начинается с промаха: источник возвращает лампу с остатком 8, копия записывается на тридцать секунд. Повтор через несколько секунд читает cache. Если основной остаток изменился до 20 внутри этого периода, копия ещё может вернуть 8. После истечения следующий GET читает primary и сохраняет 20. TTL не исправляет ответ задним числом и не уведомляет уже открытый интерфейс об изменении.
Заполнение → primary, quantity=8
До истечения → cache, quantity=8
Основные данные изменились → старая копия пока допустима
После истечения → primary, новое quantity
Значение срока связано с сервером Redis, а не таймером Node.js. Перезапуск HTTP-приложения сам по себе не превращает кеш в новый: оно снова подключится к тому же Redis и прочитает ещё существующий ключ. Это важно при локальной работе со снимками. Если запускать разные состояния против одного учебного экземпляра, они увидят ключи друг друга; архив не очищает окружение автоматически и не выполняет команды удаления.
Предел устарелости имеет условия
Фраза «копия устаревает максимум на тридцать секунд» предполагает, что источник действительно возвращает актуальное состояние и заполнение не затягивается. Если чтение источника началось раньше изменения, завершилось позднее и затем записало старую копию, срок отсчитывается уже от позднего SET. В следующем уроке поколение ключа ограничит влияние такой гонки: старое вычисление попадёт в старое пространство имён и не станет текущей копией.
Истечение не означает, что весь интерфейс автоматически обновился. Страница, уже получившая JSON, продолжает показывать своё представление до следующего запроса. Если необходима быстрая актуализация, продукт должен определить момент перечитывания или уведомление об изменении. TTL относится к серверному ключу, а не к DOM браузера. У этих копий разные владельцы и разные события обновления, хотя человек видит один каталог.
Если одновременно истекли популярные ключи, множество запросов может обратиться к источнику. В большом приложении иногда добавляют небольшой случайный разброс TTL, чтобы заполнения распределялись во времени. Это уменьшает синхронность, но не устраняет параллельные промахи одного ключа. Для маленького каталога сохраняем ровно тридцать секунд, чтобы механизм оставался наблюдаемым и не зависел от случайного значения.
Повторный SET без EX может убрать прежний срок ключа и вернуть бессрочное хранение, если не выбрана специальная политика сохранения TTL. Поэтому все пути обновления cache-ключа должны использовать один согласованный способ записи. Наличие срока после первого заполнения не гарантирует, что другой обработчик его не отменит. В нашем примере cache.js является единственным владельцем записи копии; воркер изменяет основные данные и поколение.
Для диагностики различайте отсутствующий ключ и повреждённое значение. Если GET вернул null, функция ожидаемо читает источник. Если в string записан невалидный JSON, JSON.parse выбросит ошибку, и этот снимок вернёт отказ сервиса. В production можно удалить только повреждённый cache-ключ и повторно заполнить его, но нельзя таким fallback скрывать повреждение основных данных. Восстановление заменяемой копии и исправление бизнес-записи имеют разный риск.
Политика памяти также существует отдельно от TTL. Вытеснение может удалить копию раньше срока, и тогда получится обычный промах. Но очередь BullMQ и основные остатки позднее потребуют noeviction, чтобы Redis не удалял их произвольно. В одной учебной базе TTL кеша позволяет освобождать копии, а policy noeviction сохраняет другие категории. При нехватке памяти операции могут завершиться ошибкой, которую приложение должно обработать.
Единицы EX и поведение SET описаны в документации Redis SET. После второго состояния копия имеет конечный срок и понятный путь обновления после истечения. Дальше свяжем её актуальность с поколением основных данных, чтобы успешная запись не ждала конца предыдущего периода и не зависела от запоздавшего заполнения старой копии.