Стратегии кэширования
Cache-aside, read-through, write-through, write-back и write-around не взаимозаменяемы: каждая по-своему прокладывает пути чтения и записи и даёт своё обещание — и нарушает своё — про свежесть, цену промаха и поведение при смерти кэша.
Команда добавила кэш перед сервисом товаров и увидела падение p99 на 80% — явная победа, выкаченная в пятницу. В понедельник поддержка сообщила, что у горстки товаров цена устарела на трое суток, а одному клиенту выставили неверную сумму. Ничего не «сломалось»: кэш делал ровно то, что построили — грузил значение при первом чтении и больше никогда его не обновлял. Они выбрали стратегию кэширования, не осознав, что выбор вообще был. Вопрос не «кэшировать ли?», а «где данные входят в кэш, где выходят и кто отвечает за их честность?»
Стратегия кэширования — это схема разводки двух путей
Когда ставишь кэш перед медленным хранилищем, надо ответить на два независимых вопроса. При чтении: кто идёт в базу при промахе кэша — приложение или сам кэш? При записи: куда попадает новое значение — в кэш, в базу или в оба, и в каком порядке? Именованные стратегии — cache-aside, read-through, write-through, write-back, write-around — это просто небольшой набор разумных ответов на эти два вопроса. Это не флажки, а сам поток данных, и выбор одной фиксирует твою гарантию свежести, штраф за промах и поведение при смерти узла кэша.
Пять стратегий делятся чисто: cache-aside и read-through — паттерны пути чтения (как данные попадают в кэш), а write-through, write-back и write-around — паттерны пути записи (что запись делает с кэшем). Реальные системы комбинируют по одному из каждого — самая частая боевая пара — cache-aside на чтении с write-around или TTL на записи.
К концу урока ты поймёшь, какой именно выбор сделала команда из вступления — и как делать его осознанно для своих данных.
Cache-aside (lazy loading): оркестрирует приложение
В cache-aside — AWS зовёт это lazy loading — логикой владеет приложение. При чтении: спроси кэш; при попадании верни; при промахе сходи в базу, запиши результат в кэш и верни. Кэш — тупой key-value ящик, ничего не знающий про твою базу.
ЧТЕНИЕ (cache-aside):
v = cache.get(key)
if v is null: # промах
v = db.read(key) # поход в БД
cache.set(key, v, ttl) # заполнить
return vДостоинства — ровно те, что перечисляет AWS: кэшируются только запрошенные данные (рабочий набор заполняется естественно, ты не греешь то, что никто не читает), и сбой узла переживаем — свежий пустой узел просто даёт промахи, которые его заполняют, с деградацией задержки, но без сбоя. Цена: каждый промах — штраф в три похода (промах кэша, чтение БД, запись в кэш), и поскольку при изменении базы кэш не обновляется, кэшированные значения устаревают, пока не добавишь TTL или инвалидацию. Это устаревание и есть баг из вступления.
Read-through: оркестрирует кэш
Read-through переносит логику промаха в слой кэша (или библиотеку/провайдер перед ним). Приложение всегда зовёт только cache.get(key); при промахе сам кэш грузит из базы, сохраняет и возвращает. Поток данных идентичен cache-aside — разница в том, кто пишет код. Read-through централизует логику загрузки, чтобы все вызывающие вели себя одинаково и нельзя было выкатить сервис, забывший заполнить кэш при промахе; плата — нужен провайдер кэша с поддержкой этого и теряется свобода формировать каждый запрос.
▸Почему это работает
Почему cache-aside доминирует, несмотря на аккуратность read-through? Потому что cache-aside держит кэш и базу расцепленными: кэш может быть общим Redis/Memcached, приложение может кэшировать вычисленное значение (склеенный, отрендеренный или агрегированный объект без единой строки БД за ним), и логику загрузки можно менять для каждой точки вызова. Read-through сцепляет кэш с известным загрузчиком — чисто для «кэшируй одну таблицу по первичному ключу», но неудобно, когда кэшируемый объект выводится из пяти запросов. Большинство крупных систем выбирают расцепленный путь и платят за него дисциплиной — каждая точка записи обязана инвалидировать или ставить TTL — и поэтому инвалидация становится самым трудным уроком раздела.
Три стратегии записи: куда ложится запись
Когда ты трогаешь путь записи, ты решаешь не просто куда сохранить данные, а насколько кэш останется честным, какую производительность получишь и чем рискуешь при сбое. У этого решения есть имя. На пути записи вопрос — что write(key, value) делает с кэшем. Три ответа:
- Write-through — пиши кэш и базу синхронно, на каждой записи. Кэш никогда не устаревает (обновляется вместе с БД), но каждая запись платит два похода, и кэш заполняется данными, которые могут вообще не прочитать (cache churn) — поэтому write-through обычно идёт с TTL, вытесняющим непрочитанный мусор. AWS отмечает компромисс ровно так: записи замедляются, но пользователи терпят задержку записи лучше, чем чтения.
- Write-back (write-behind) — пиши кэш сейчас, верни вызывающему и сбрось в базу асинхронно (батчем, позже). Это даёт минимальную задержку записи и поглощает всплески записи, склеивая много обновлений в меньше походов в БД. Опасность жестокая: если узел кэша умрёт до сброса, эти подтверждённые записи пропадут. Write-back меняет надёжность на скорость, поэтому подходит метрикам, счётчикам и просмотрам — не деньгам.
- Write-around — пиши только базу, минуя кэш; значение войдёт в кэш позже, лениво, при следующем чтении (через cache-aside). Это избегает забивания кэша данными с интенсивной записью, которые скоро не читают, ценой гарантированного промаха на первом чтении после записи. Подходит данным «записал-раз-читаю-может-быть» вроде логов.
Вместе эти три стратегии означают, что ты меняешь задержку записи, надёжность и свежесть между собой. Когда встретишь write-back в проектировании, первый вопрос — какие именно данные мы можем позволить себе потерять, если узел упадёт до сброса?
Сервис оформления заказов кеширует цены товаров. Обновление цены должно отображаться в течение нескольких секунд, кеш должен переживать перезапуск Redis без выдачи неверных данных, а задержка записи должна оставаться ниже 50 мс. Какая стратегия записи подходит?
Консистентность: что обещает и нарушает каждая стратегия
Весь смысл выбора в том, что каждая стратегия даёт своё обещание свежести:
- Cache-aside / write-around / read-through все допускают устаревшие чтения между изменением БД и следующим обновлением кэша — им нужен TTL или явная инвалидация, чтобы ограничить устаревание. Это частый, принятый компромисс: пара секунд устаревания за огромную пропускную способность чтения.
- Write-through — единственная, кто держит кэш непрерывно консистентным с БД на записях — но она не защищает от обновления, обходящего кэш (батч-задача, второй сервис, пишущий прямо в БД), что тихо возвращает устаревание.
- Write-back неконсистентна с БД по дизайну во время окна сброса и рискует потерей данных при сбое кэша — самая сильная производительность, самая слабая надёжность.
Senior-формулировка: нет варианта «свежо, быстро и надёжно»; ты выбираешь, чем из трёх пожертвовать, для каждого класса данных. Цены и балансы хотят write-through или явной инвалидации; список трендов норм на cache-aside с коротким TTL; счётчик просмотров — хрестоматийный write-back.
▸Частая ошибка
Дорогая ошибка — применять одну стратегию ко всем данным. Команды выбирают «cache-aside с TTL в час» глобально и потом обнаруживают, что баланс счёта пользователя кэшируется так же, как баннер на главной — и платёж всплывает с часовым опозданием. Кэширование — поклассовое: классифицируй каждую кэшируемую вещь по тому, сколько устаревания она терпит и насколько дорого неверное значение. Дорогие, нетерпимые данные (деньги, права, остаток на чекауте) получают write-through или активную инвалидацию; дешёвые, терпимые (рекомендации, счётчики, отрендеренные фрагменты) — cache-aside с TTL. Выбрать одну глобальную стратегию — значит выбрать неверную для большинства своих данных.
Сервис кэширует балансы счетов через cache-aside с TTL в час, без инвалидации. Пользователь пополняет баланс записью, идущей прямо в базу. Что покажет следующее чтение и почему?
Нужна минимально возможная задержка записи для высоконагруженного счётчика просмотров, и пара секунд потери данных при редком крахе кэша приемлема. Какая стратегия записи подходит и каков её риск?
В _______ кэшировании (AWS зовёт это lazy loading) приложение грузит значение из базы только при промахе чтения, затем сохраняет — поэтому кэшируются лишь запрошенные данные, а пустой узел просто дозаполняется, но значения устаревают, пока их не ограничит TTL или инвалидация.
- 01Сравни cache-aside и read-through — поток данных одинаков, что отличается?
- 02Сравни write-through, write-back и write-around на пути записи.
- 03Какую свежесть/надёжность обещает каждая стратегия и как выбирать?
Стратегия кэширования — это схема разводки двух путей, а не флажок. На пути чтения cache-aside (lazy loading) даёт приложению грузить из БД при промахе и заполнять кэш — кэшируются лишь запрошенные данные, переживаются пустые узлы, но допускаются устаревшие чтения; read-through переносит ту же логику загрузки-при-промахе в слой кэша ради консистентности ценой сцепления. На пути записи write-through пишет кэш и БД синхронно (никогда не устаревает, но два похода и churn — ставь TTL), write-back пишет кэш сейчас и сбрасывает БД позже (минимальная задержка, склеивает всплески, но теряет несброшенные записи при крахе), write-around пишет только БД и пускает значение в кэш лениво (без churn, но гарантированный промах на первом чтении). Каждая даёт своё обещание про свежесть, цену промаха и надёжность, и варианта «быстро-свежо-надёжно» разом нет — поэтому ты комбинируешь один паттерн чтения и один записи на каждый класс данных, никогда одну глобальную стратегию на всё. Ограничение устаревания, что допускают ленивые паттерны, — тема остального раздела: вытеснение и TTL, а затем по-настоящему трудная проблема инвалидации. Теперь, когда встретишь кэш-баг в проде, первый вопрос такой: какая стратегия была выбрана и было ли окно устаревания тем, на что команда реально соглашалась?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.