Инвалидация и проблемы кэша
Почему про это спрашивают
Представь кафе с меловой доской «Сегодня в меню». Повар убрал сырники, а доску не стёр. Гости заказывают сырники, официант уходит на кухню и возвращается с извинениями. Кэш — это и есть такая доска: быстрая копия правды. Главный вопрос не «как её повесить», а «кто и когда её стирает».
Есть известная шутка: в программировании две сложные вещи — инвалидация кэша и придумывание имён. На собесе кэш предлагают почти все. Сильного кандидата отличает другое: он сам говорит, как кэш будет устаревать и ломаться.
Почему инвалидация — самое сложное
Как только появился кэш, у тебя два источника правды: база и кэш. Обновить их одновременно и атомарно нельзя, это разные системы. Между двумя записями всегда есть окно, и в него влезают гонки.
Классическая гонка при подходе «прочитал из БД → положил в кэш»:
Три основных способа инвалидации:
| Способ | Как работает | Плюс | Минус |
|---|---|---|---|
| TTL | ключ сам истекает через N секунд | просто, самоисцеляется | до N секунд устаревших данных |
| Удаление при записи | после UPDATE в БД делаем DEL key |
свежесть почти сразу | гонки, забытые места записи |
| Версия в ключе | user:42:v7, при изменении версия растёт |
старое просто перестаёт читаться | нужно где-то хранить версию |
💡 Правило: при записи удаляй ключ, а не обновляй его. Если два писателя обновляют кэш, порядок может перепутаться. После удаления следующий читатель возьмёт свежие данные из БД. И всегда добавляй TTL как страховку: даже если удаление потерялось, ошибка проживёт ограниченное время.
Устаревшие данные: как с ними жить
Идеальной свежести не бывает. Решай отдельно для каждого типа данных, сколько устаревания терпимо:
- лента новостей, счётчик лайков: минуты, всем всё равно;
- профиль пользователя: секунды, плюс удаление при записи;
- баланс, остаток на складе при оплате: не кэшируем или сверяем с БД в момент транзакции.
Полезный приём — read-your-writes. Пользователь, который сам что-то поменял, должен сразу это увидеть. Для него можно читать мимо кэша несколько секунд после записи. Остальные спокойно подождут TTL.
Cache stampede: лавина при истечении
Популярный ключ истёк, и в ту же миллисекунду тысячи запросов получают промах и все идут в базу пересчитывать одно и то же.
Посчитаем масштаб:
| Параметр | Значение |
|---|---|
| Нагрузка на ключ | 10 000 RPS |
| Время пересчёта в БД | 200 мс |
| Запросов в БД за окно пересчёта | 10 000 × 0,2 = 2 000 одинаковых запросов |
База легла, запросы ждут, таймауты, ретраи, и стало ещё хуже.
Три защиты
1. Блокировка (single-flight). Пересчитывает только первый запрос. Он берёт лок SET lock:key 1 NX EX 5, остальные ждут или получают старое значение. В БД уходит 1 запрос вместо 2 000. Цена: лишняя задержка у ждущих и риск, что держатель лока упадёт. Поэтому у лока обязательно есть TTL.
2. Ранний перерасчёт. Обновляем ключ до истечения. Например, храним рядом «мягкий» срок. Когда он прошёл, один запрос обновляет значение в фоне, а все остальные продолжают получать текущее (stale-while-revalidate). Есть и вероятностный вариант: чем ближе к концу TTL, тем выше шанс, что очередной запрос запустит обновление. Цена: лишние пересчёты и усложнение логики.
3. Джиттер TTL. Если ты прогрел 100 000 ключей с одинаковым TTL=300, через 5 минут они истекут все разом. Добавь случайность, например TTL = 300 + random(0, 60), и истечения размажутся по минуте. Это дёшево и почти бесплатно, поэтому джиттер стоит упоминать всегда.
Горячие ключи
Даже без истечения один ключ (пост звезды, товар дня) может получать столько трафика, что один шард Redis упирается в CPU или сеть, хотя остальные шарды простаивают. Шардирование тут не помогает: ключ всё равно живёт на одном узле.
Как с этим справиться:
- локальный in-memory кэш в каждом инстансе API с коротким TTL поглощает основную массу запросов;
- копии ключа
key#0..Nлежат на разных шардах, чтение идёт со случайной копии (минус: инвалидировать приходится все N копий); - реплики для чтения в Redis;
- детекция: мониторинг топа ключей, чтобы горячие находить заранее.
Как это звучит на собесе
Когда добавляешь кэш, проговори пять пунктов:
- Что кэшируем и зачем: «профили, read:write ≈ 100:1, снимем 90% нагрузки с БД».
- Стратегия: cache-aside, при записи
DEL, плюс TTL как страховка. - Допустимая устарелость: «для профиля до 60 секунд ок, баланс не кэшируем».
- Stampede: «для популярных ключей single-flight лок и джиттер TTL».
- Горячие ключи: «локальный кэш на пару секунд, при необходимости реплицируем ключ».
🎯 Фраза-маркер синьора: «Кэш — это компромисс между свежестью и нагрузкой. Давайте определим, сколько устаревания мы готовы терпеть». После неё интервьюер обычно кивает.
Частые ошибки
- Предлагать кэш и молчать про инвалидацию.
- Обновлять ключ при записи вместо удаления (гонки).
- Ставить ключи без TTL («удалим вручную»).
- Одинаковый TTL у массово прогретых ключей.
- Кэшировать то, где устаревание недопустимо (деньги, остатки при покупке).
- Думать, что шардирование решает проблему горячего ключа.
Если ты можешь назвать гонку, лавину и горячий ключ и для каждого дать защиту с ценой, то тема кэширования на собесе у тебя закрыта.