open atlas
↑ К треку
Основы System Design SD · 05 · 04

Инвалидация кэша

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

SD Middle ◷ 20 min
Уровень
ОсновыJuniorMiddleSenior

Команда выкатила явную инвалидацию: каждый раз при изменении базы они удаляли соответствующий ключ кэша, чтобы следующее чтение перезагрузило свежее. Чище, чем TTL, думали они. Неделями работало. Потом пользователь сообщил, что его старое фото профиля всё возвращается через минуты после смены. Причина — гонка: чтение промахнулось по ключу и начало грузить старое значение из базы ровно в тот миг, когда запись обновила базу и удалила ключ. Медленное чтение завершилось последним и записало устаревшее значение, что оно достало, обратно в теперь пустой кэш — заново отравив его на весь TTL. Ничего не сломалось; порядок просто не повезло. Вот почему «в computer science две трудные вещи: инвалидация кэша и именование» — это шутка, которую инженеры рассказывают, морщась.

Почему инвалидация — трудная проблема

Каждый кэш держит копию данных, живущих где-то ещё. В момент изменения оригинала копия неверна — а кэш не в курсе, ведь (как показал урок про стратегии) ленивые кэши не следят за базой. Инвалидация кэша — это дисциплина вынуть неверную копию из кэша, вовремя, не сломав корректность и не молотя бэкенд. Она знаменито трудна не потому, что какая-то одна техника сложна, а потому, что сидит ровно там, где две независимые временны́е линии — твои записи и твои чтения — непредсказуемо переплетаются по распределённой системе. Есть три семейства подходов, и каждое меняет одни и те же две вещи: устаревание (насколько неверным может быть чтение?) против сложности/сцепления (сколько путь записи должен знать о кэше?).

Три подхода

Прежде чем выбирать, спроси себя: насколько неверным может быть чтение и кто заплатит, если инвалидация пропустила точку? Ответ скажет, какой компромисс тебе подходит.

  • Истечение по TTL — пусть каждая запись истекает через N секунд; путь записи не делает ничего. Это проще всего и максимально расцеплено: кэш самоисцеляется, писатели его не трогают. Цена — ограниченное устаревание: значение может быть неверным до TTL. Верный дефолт для данных, терпящих секунды-минуты лага (ленты, листинги, отрендеренные фрагменты).
  • Явная инвалидация — на каждой записи активно удалять (или обновлять) затронутый ключ кэша, чтобы следующее чтение перезагрузило. Устаревание падает к нулю, но теперь путь записи сцеплен с кэшем: он должен знать каждый ключ, выведенный из изменённых данных, и единственная пропущенная точка инвалидации оставляет ключ устаревшим навсегда (до его TTL, если есть). В этом сцеплении живёт большинство багов кэша.
  • Версионированные ключи — впеки версию в сам ключ: user:42:v7. Запись ничего не удаляет; она инкрементит версию (v7 → v8), так что чтения новой версии просто промахиваются по ключу, что никогда не записывался, и грузят свежее, а старые записи v7 теперь сироты, со временем вытесняемые. Это полностью обходит гонку удаления (ты никогда не мутируешь существующий ключ) ценой оставленного мусора на вытеснение и нужды где-то отслеживать текущую версию.

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

Почему это работает

Почему явная инвалидация гоняется, и почему версионирование уходит от гонки? Гонка — классический переплёт чтения-записи: читатель промахивается по ключу K и начинает медленную загрузку старого значения БД; тем временем писатель обновляет БД и удаляет K. Если медленное чтение завершается после удаления, оно записывает старое значение обратно в пустой кэш — устаревшая запись, созданная корректным кодом в невезучем порядке (вступление). Удаление это не починит, ведь читатель уже держит устаревшее значение в полёте. Версионированные ключи уходят от этого, потому что писатель инкрементит до v8, а устаревшее значение читателя пишется в старый ключ v7 — который ни одно новое чтение не запросит. Новая версия — свежий, никогда не записанный ключ, так что её первое чтение — чистый промах против текущего состояния БД. Ты сменил «мутируй общий ключ и надейся, что порядок добрый» на «пиши в новёхонький ключ на версию», у которого нет порядка, чтобы ошибиться.

Устаревшие чтения, негативное кэширование и dogpile

Ещё три опасности сидят вокруг инвалидации:

  • Устаревшие чтения — принятая цена TTL/ленивых стратегий, но они становятся багом корректности, когда класс данных их не терпит (баланс-показанный-с-часовым-опозданием из урока про стратегии). Починка не «никогда не устаревать» — а сопоставить бюджет устаревания данным: ограничь коротким TTL или плати цену сцепления явной инвалидации там, где это реально важно.
  • Негативное кэширование — кэшировать отсутствие значения («не найдено»), чтобы повторные поиски отсутствующего ключа не молотили базу каждый раз. Жизненная защита от потока промахов по несуществующим ключам (например, атакующий запрашивает случайные ID), но у неё своя проблема инвалидации: когда сущность наконец создаётся, слишком долгий TTL негативного кэша её прячет. Негативным записям нужны короткие TTL или явная инвалидация при создании.
  • Dogpile — стампида из урока про распределённый кэш сама по себе событие инвалидации: инвалидация (удаление) горячего ключа создаёт мгновенный зазор, который N параллельных читателей бросаются заполнить. Так что инвалидация и защита от стампиды сцеплены: агрессивное «удаляй на каждой записи» на горячем ключе и есть триггер стампиды — поэтому горячие ключи часто предпочитают обновление-на-месте или версионирование удалению.

Глубочайший анти-паттерн: кэш как источник истины

Самая опасная ошибка — не гонка; это категориальная ошибка. Кэш — это одноразовая копия авторитетных данных, живущих в надёжном хранилище. Считай его источником истины — пиши только в кэш и лениво сбрасывай, или предполагай «если оно в кэше, оно верно» — и ты построил систему, теряющую данные при перезапуске кэша (он не надёжен), отдающую что попало закэшировалось, даже когда БД не согласна, и неперестраиваемую с нуля, ведь никто не знает реального состояния. Каждая стратегия инвалидации выше предполагает, что база авторитетна, а кэш расходуем: ты должен иметь возможность сбросить весь кэш в любой момент и чтобы система восстановилась (медленно, через промахи) к корректным ответам. Если сброс кэша потерял бы данные или отдавал бы неверные результаты бесконечно — это не кэш, а ненадёжная primary-база, и ты унаследовал все её проблемы консистентности без единой её гарантии.

Частая ошибка

Конкретная версия ловушки источника-истины: использовать write-back кэширование (из урока про стратегии) и потом читать из кэша как из авторитетного, пока асинхронный сброс в БД лагает или тихо падает. Кэш показывает «верное» число, база — старее, и любой процесс, читающий БД напрямую (отчёт, второй сервис, бэкап), видит неверное значение — а если кэш вытеснит или упадёт до сброса, подтверждённая запись просто пропала. Правило, предотвращающее весь этот класс багов: записи подтверждаются только когда они в надёжном хранилище, а кэш считается слоем производительности, который можно сбросить в любой момент. Кэш делает чтения быстрыми; он не делает данные существующими.

Викторина

С явной инвалидацией «удаляй ключ на каждой записи» устаревшее значение пользователя изредка возвращается сразу после обновления. Что происходит и какой подход убирает гонку?

Викторина

Архитектор предлагает писать все обновления только в Redis и сбрасывать в Postgres асинхронно, затем читать из Redis как авторитетное значение. Каково ключевое возражение?

Закончи аналогию

Чтобы инвалидировать без гонки удаления, впеки версию в ключ (user:42:v7) и инкременть её на каждой записи; это работает, потому что устаревшая запись читателя ложится на теперь осиротевшую старую _______, которую ни одно новое чтение не запросит — нет общего мутируемого ключа, за который дрались бы две линии.

Вспомните перед уходом
  1. 01
    Почему инвалидация кэша трудна и что меняют три подхода?
  2. 02
    Объясни гонку на записи и почему версионированные ключи её избегают.
  3. 03
    Что такое негативное кэширование, связь dogpile-как-инвалидации и анти-паттерн источника истины?
Итог

Инвалидация кэша — по-настоящему трудная часть кэширования: не потому, что какая-то одна техника сложна, а потому, что она живёт там, где твои линии чтения и записи переплетаются по распределённой системе, а кэш тихо держит копию, что становится неверной в момент изменения оригинала. Три подхода меняют устаревание на сложность/сцепление: TTL (путь записи ничего не делает, самоисцеляется, но ограниченно устаревает), явная инвалидация (удаление на записи, почти свежо, но путь записи сцеплен с каждым выведенным ключом и пропущенная точка устаревает навсегда), версионированные ключи (инкремент версии на записи, без гонок, но оставляет сирот). Гонка на записи — медленное чтение, пишущее старое значение обратно после инвалидирующего удаления — и есть причина, почему версионирование, никогда не мутирующее общий ключ, бьёт удаление; практический дефолт — явная инвалидация с TTL-подстраховкой. Вокруг сидят устаревшие чтения (сопоставь бюджет классу данных), негативное кэширование (кэшируй отсутствие, но с короткими TTL, чтобы создания не прятались) и dogpile (инвалидация горячего ключа и есть триггер стампиды). Глубочайшая ловушка — категориальная ошибка считать кэш источником истины: кэш — одноразовая копия авторитетного надёжного хранилища, и ты должен мочь сбросить его целиком в любой момент и восстановить корректные ответы — иначе ты построил ненадёжную базу в одежде кэша. На этом раздел про кэширование завершён: стратегия, вытеснение/TTL, распределение и честность копии. Теперь, когда встретишь в ревью системы записи, подтверждаемые до попадания в надёжное хранилище, задай один вопрос: если кэш перезапустится прямо сейчас — пропадут ли данные? Если да — перед тобой ненадёжная база в одежде кэша.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.