Инвалидация — Примеры

Пример 1 — Чтение, которое возвращает устаревшее значение обратно

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

t0  чтение  get product:7        -> промах
t1  чтение  читает источник      -> цена 19.99
t2  запись  обновляет цену       -> 24.99
t3  запись  del product:7
t4  чтение  set product:7 = 19.99, ttl 300
t5  чтение  get product:7        -> 19.99    устарело на срок до 300 с

Решение

const version = row.updated_at.getTime();     // меняется с каждой записью
const key = `product:${id}:v${version}`;

await cache.set(key, JSON.stringify(row), "EX", 300);
// по мере сил: запись также удаляет ключ предыдущей версии

// t0..t1  чтение получает версию 3 (19.99)
// t2      запись обновляет, версия становится 4, удаляет product:7:v3
// t4      чтение пишет product:7:v3       (сюда больше никто не посмотрит)
// t5      чтение ищет product:7:v4     -> промах -> свежие 24.99

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

Пример 2 — Удаление, которое чистит больше, чем изменила запись

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

update products set price = 2499 where id = 7;

DEL product:*

Решение

// изменение цены затрагивает только текущую версию одного товара
await cache.del(`product:${id}:v${newVersion}`);

// список избранного хранит идентификаторы, а не цены, поэтому эта запись его
// не трогает; честность ему обеспечивает собственное короткое истечение
await cache.set("products:featured", ids, "EX", 60);

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