Перейти к содержанию
Innovialabs

Стратегии инвалидации кэша: TTL, write-through и защита от stampede

Продвинутое4 мин

Коротко

Для большинства систем подходит cache-aside с TTL и удалением ключа при каждой записи; горячие ключи защищайте блокировкой или single-flight, добавляйте jitter к TTL и отдавайте устаревшие данные, пока идёт фоновое обновление.

Коротко

Идеальной стратегии инвалидации нет — есть компромисс между свежестью данных, нагрузкой на базу и сложностью. Надёжный вариант по умолчанию для большинства продуктов:

  • чтение по схеме cache-aside: проверяем кэш, при промахе берём из базы и сохраняем результат;
  • TTL на каждом ключе как страховка;
  • удаление ключа после каждой записи в источник истины;
  • защита горячих ключей от stampede.

Начните с решения для каждого типа данных: насколько они могут устаревать. Описание товара может отставать на минуту, баланс или остаток на складе при оформлении заказа — нет.

Сравнение схем записи

СхемаКак работаетГде хорошаРиски
Cache-asideПриложение читает кэш, заполняет при промахе, инвалидирует при записиБольшинство API с преобладанием чтенияГонка между записью и повторным заполнением
Write-throughКаждая запись идёт в кэш и базу вместеДанные, которые читают сразу после записиМедленнее запись, кэш забивается редко читаемым
Write-behindЗапись в кэш, в базу — асинхронноОчень частые записи, счётчикиПотеря данных, если кэш упадёт до сброса

Write-behind самый рискованный: кэш на время становится источником истины. Используйте его там, где потеря последних записей допустима или очередь надёжная.

TTL против инвалидации по событиям

TTL прост и самовосстанавливается: даже если вы забыли инвалидировать, после истечения данные станут верными. Цена — гарантированное окно устаревания и периодические промахи.

Инвалидация по событиям удаляет или обновляет ключи при изменении данных: сразу после записи, через шину сообщений или через change data capture (CDC) базы. Данные свежее, но нужно знать все ключи, зависящие от записи, а потерянное событие означает устаревшие данные, пока их что-то не исправит.

На практике их комбинируют: события дают свежесть, TTL страхует от пропущенных событий.

Практические правила:

  • При записи удаляйте ключ, а не перезаписывайте. Два параллельных писателя, обновляющих кэш, могут оставить в нём более старое значение.
  • Сначала пишите в базу, затем удаляйте ключ.
  • Читатель всё ещё может вернуть старое значение сразу после удаления. Короткий TTL, повторное отложенное удаление или версионированные ключи (product:42:v17) закрывают эту дыру.
  • Кэшируйте результат «не найдено» на короткое время, чтобы запросы несуществующих ID не били в базу.

Cache stampede и как его остановить

Stampede случается, когда популярный ключ истекает и сотни запросов одновременно промахиваются, выполняя один и тот же тяжёлый запрос. База замедляется, больше запросов уходит в таймаут, и проблема раскручивает сама себя.

Рабочие приёмы:

  • Блокировка или single-flight: значение пересобирает только один запрос, остальные коротко ждут или получают старое значение.
  • Jitter для TTL: добавьте случайный разброс к сроку жизни, чтобы ключи, созданные вместе, не истекали вместе.
  • Stale-while-revalidate: продолжайте отдавать истёкшее значение, пока одна фоновая задача его обновляет.
  • Раннее обновление: обновляйте горячие ключи незадолго до истечения — по расписанию или вероятностно при чтении.
  • Прогрев известных горячих ключей после деплоя или перезапуска кэша.

Минимальная блокировка на Redis:

def get_product(pid):
    key = f"product:{pid}"
    cached = redis.get(key)
    if cached:
        return json.loads(cached)

    if redis.set(f"lock:{key}", "1", nx=True, ex=10):
        try:
            product = db.load_product(pid)
            ttl = 300 + random.randint(0, 60)  # jitter
            redis.set(key, json.dumps(product), ex=ttl)
            return product
        finally:
            redis.delete(f"lock:{key}")

    time.sleep(0.05)
    cached = redis.get(key)
    return json.loads(cached) if cached else db.load_product(pid)

Для HTTP-ответов та же идея встроена в заголовок Cache-Control: max-age=60, stale-while-revalidate=300 позволяет CDN и браузерам отдавать слегка устаревшую копию, пока загружается свежая.

Как выбрать

  1. Разделите данные по допустимому устареванию: секунды, минуты, часы, никогда.
  2. Данные, которые не могут устаревать (деньги, остаток при оформлении), читаются из базы или сверяются с ней; кэш — только подсказка.
  3. Для остального — cache-aside с TTL и удалением при записи.
  4. События добавляйте только там, где устаревание по TTL заметно мешает пользователям.
  5. Следите за hit rate, задержкой при промахах и нагрузкой на базу. Падение hit rate после релиза часто означает, что изменился формат ключа.

Частые вопросы

Обычно удалять. Обновление требует одинаковой сериализации и порядка операций везде и после параллельных записей может оставить старое значение. Удаление позволяет следующему чтению собрать значение из источника истины.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.