Инвалидация — Примеры
Пример 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);
Удаление по шаблону сканирует всё пространство ключей, может блокировать другие операции, а здесь ещё и уничтожает долю попаданий, которой кэш и оправдывался. Инвалидацию нужно выводить из того, что изменилось: цена затрагивает один ключ товара, а список, который хранит только идентификаторы, от цены вообще не зависит, поэтому шестидесяти секунд истечения ему достаточно. Обычно же пространство ключей очищают целиком, чтобы почувствовать себя в безопасности, и меняют небольшой риск корректности на большой риск производительности.