В прошлом уроке запись шла мимо кэша: приложение обновляло базу и выбрасывало копию из Redis. Следующий читатель промахивался и собирал копию заново. Это не единственный способ. Запись может проходить через кэш, и тогда возникает вопрос: когда данные доходят до базы, сразу или потом?
Официант и кухня
Официант принимает заказ. Первый вариант: он записывает блюдо в блокнот и сразу несёт листок на кухню, дожидается, пока повар кивнёт, и только тогда говорит гостю «принято». Медленнее, зато кухня знает о каждом заказе.
Второй вариант: официант записывает заказы в блокнот и относит их на кухню пачкой раз в пять минут. Гостей он обслуживает быстрее, и повара не дёргают по каждой мелочи. Но если официант потеряет блокнот по дороге, заказы за эти пять минут пропадут.
Блокнот здесь это Redis, кухня это база данных. Первый вариант называется write-through, второй write-behind.
Write-through: сразу в оба места
При write-through приложение при изменении пишет и в базу, и в кэш, и только потом отвечает «готово». Копия в кэше появляется сразу, её не надо ждать до первого промаха.
Смена цены из прошлого урока в таком виде. Клиенты redis и pg те же, getBook остаётся прежним:
async function updatePrice(id, price) {
const { rows } = await db.query(
'UPDATE books SET price = $2 WHERE id = $1 RETURNING id, title, price',
[id, price],
);
await redis.set(`cache:book:${id}`, JSON.stringify(rows[0]), { EX: BOOK_TTL });
}
RETURNING из урока про UPDATE возвращает строку уже с новой ценой, и её же мы кладём в кэш. Вызовем updatePrice(1, 620), а потом getBook(1):
hit cache:book:1
{ id: 1, title: 'Master and Margarita', price: '620.00' }
Сразу попадание, и цена новая. В redis-cli это выглядит как обычный SET поверх старой копии:
127.0.0.1:6379> SET cache:book:1 "{\"id\":1,\"title\":\"Master and Margarita\",\"price\":620}" EX 300
OK
127.0.0.1:6379> GET cache:book:1
"{\"id\":1,\"title\":\"Master and Margarita\",\"price\":620}"
Чем платим:
- Запись медленнее. Каждое изменение ждёт двух хранилищ вместо одного.
- Кэш забивается. В него попадают и те книги, которые никто не откроет. Спасает TTL: невостребованная копия истечёт сама.
- Две записи не атомарны. База обновилась, а Redis в этот момент недоступен: в кэше осталась старая цена. Транзакция PostgreSQL на Redis не распространяется. И здесь выручает TTL.
- Гонка двух записей. Два администратора меняют цену почти одновременно. В базу последней попала цена второго, а в кэш могла последней попасть цена первого. При удалении копии, как в cache-aside, такой гонки нет: обе записи удаляют один и тот же ключ, и порядок не важен.
Поэтому чистый write-through встречается реже, чем кажется. Часто его смешивают с cache-aside: чтение идёт через getBook, а запись либо удаляет копию, либо перезаписывает её, если важно, чтобы первый же читатель после изменения не ходил в базу.
урок из курса
Урок из курса «Redis с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.