Паттерны кэширования — Примеры
Пример 1 — Кэш, читаемый при промахе, с честным сроком жизни
Страница товара читает одни и те же несколько строк гораздо чаще, чем они меняются, а сейчас до базы доходит каждое чтение. Поставьте рядом с чтением кэш (cache-aside, кэш в стороне), наполняйте его при промахе — и выберите срок жизни, который честно заявляет, насколько устаревшей может быть страница товара.
async function getProduct(id) {
return db.query("select * from products where id = $1", [id]);
}
Решение
async function getProduct(id) {
const key = `product:${id}`;
const cached = await cache.get(key);
if (cached) return JSON.parse(cached);
const product = await db.query("select * from products where id = $1", [id]);
const ttl = 300; // секунды: максимум, на который цена может отстать от записи
await cache.set(key, JSON.stringify(product), "EX", ttl);
return product;
}
К источнику путь чтения обращается только при промахе, поэтому кэш наполняется данными, которые действительно запрашивают. Пять минут — решение настоящее: оно ограничивает, как долго отдаётся устаревшая цена, даже если инвалидацию пропустили. Единственным источником истины остаётся база, так что потерянный ключ стоит одного запроса. Ошибаются обычно на другом: кэшируют вовсе без срока жизни. Это выглядит быстрее, но превращает пропущенную инвалидацию в неограниченную устарелость.
Пример 2 — Истечение, которое приходит под нагрузкой
Главная страница читает один ключ избранного товара с фиксированным сроком жизни в час. В начале каждого часа все запросы промахиваются по нему одновременно, и база получает тот самый всплеск, который кэш должен был убрать. Измените устройство так, чтобы источник видел одно чтение вместо каждого запроса.
const key = "product:featured";
const ttl = 3600; // один фиксированный час
await cache.set(key, body, "EX", ttl);
Решение
const key = "product:featured";
const ttl = 3600 + Math.floor(Math.random() * 300); // сроки жизни разнесены
// ключ всё ещё слишком горяч, чтобы дать ему истечь под нагрузкой:
// обновляет один вызывающий, а остальные читают копию, когда она появится
const gotLock = await cache.set("lock:product:featured", "1", "NX", "EX", 10);
if (gotLock) {
const body = await db.query("select * from products where featured = true");
await cache.set(key, JSON.stringify(body), "EX", ttl);
}
Разнесение срока жизни не даёт ключам, записанным в одну секунду, истечь в одну и ту же секунду, а короткая блокировка позволяет одному вызывающему заплатить за обновление, пока остальные ждут копию. Свой срок жизни есть и у блокировки, поэтому упавший обновляющий не удержит её вечно. Обычно же кэш считают защитой и не спрашивают, что случится при промахе, — а популярный ключ, который истекает под нагрузкой, делает отказ хуже, а не лучше.