Продвинутый

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

Урок 4 из 5 в курсе Кэширование в backend-разработке

Содержание курса (4/5)

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

«В компьютерных науках есть только две сложные проблемы: инвалидация кэша и именование» — Фил Карлтон. Этот урок разбирает стратегии, чтобы кэш всегда содержал актуальные данные.

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

Главная идея

Инвалидация — это удаление или обновление записи в кэше при изменении исходных данных. Основные стратегии: TTL (автоматическое истечение), event-driven (удаление при записи), version-based (ключ содержит версию).

Как это выглядит на практике

  1. Администратор меняет цену товара в панели управления
  2. Контроллер сохраняет новую цену в БД
  3. After-save callback удаляет кэш товара: REDIS.del('product:42')
  4. Callback также удаляет кэш каталога: REDIS.del('catalog:page:1')
  5. Следующий запрос на товар 42 — cache miss, свежие данные из БД
  6. Каталог тоже обновляется при следующем запросе с новой ценой

Примеры кода

Стратегии инвалидации

class Product < ApplicationRecord
  # 1. Event-driven: удаление при изменении
  after_save :invalidate_cache
  after_destroy :invalidate_cache

  private

  def invalidate_cache
    # Удаляем кэш самого товара
    Rails.cache.delete("product:\#{id}")

    # Удаляем кэш каталога по паттерну
    Rails.cache.delete_matched("catalog:*")

    # Удаляем кэш категории
    Rails.cache.delete("category:\#{category_id}:products")
  end
end

# 2. Version-based: ключ содержит версию
def cached_product(product)
  key = "product:\#{product.id}:v\#{product.updated_at.to_i}"
  Rails.cache.fetch(key, expires_in: 1.hour) do
    ProductSerializer.new(product).as_json
  end
end
# Старая версия автоматически вытесняется через LRU

Каскадная инвалидация с тегами

# Rails cache tags (с Redis store)
class ProductsController < ApplicationController
  def index
    @products = Rails.cache.fetch(
      'catalog:all',
      expires_in: 10.minutes,
      tags: ['products']        # тег для групповой инвалидации
    ) do
      Product.published.includes(:category).to_a
    end
  end
end

class Product < ApplicationRecord
  after_save do
    # Удалить ВСЕ записи с тегом 'products' одним вызовом
    Rails.cache.delete_matched_tags('products')
  end
end

Что происходит под капотом

  • TTL — простейшая стратегия: данные устаревают гарантированно, но могут быть stale до истечения
  • Event-driven — точная, но хрупкая: забыли callback — получили stale data
  • Version-based — изящная: новая версия = новый ключ, старые вытесняются через LRU
  • delete_matched с паттерном (catalog:*) может быть медленным при большом количестве ключей
  • При каскадных связях (товар → категория → каталог) инвалидация быстро усложняется

Типичные ошибки и заблуждения

  • «TTL достаточно для всего» — для критичных данных (цена, баланс) пользователь не должен видеть устаревшие данные даже 1 секунду
  • «Можно обновить кэш вместо удаления» — обновление создаёт race condition, если два процесса обновляют одновременно
  • «Cache tag — серебряная пуля» — массовая инвалидация по тегу может обнулить весь кэш и вызвать cold start

Ключевые выводы

  • TTL — базовая защита от бесконечно устаревшего кэша, устанавливайте всегда
  • Event-driven инвалидация (after_save/after_destroy) — точная, но требует дисциплины
  • DEL > SET при инвалидации: удаление безопаснее обновления из-за race conditions

Термины урока

{:term=>"Stale data", :definition=>"Устаревшие данные в кэше, не соответствующие текущему состоянию источника"}
{:term=>"Cache tag", :definition=>"Метка, объединяющая группу кэш-записей для одновременной инвалидации"}
{:term=>"Race condition", :definition=>"Ситуация, когда результат зависит от порядка выполнения параллельных операций"}

Связь с работой backend-разработчика

Используйте TTL как safety net и event-driven инвалидацию для критичных данных. Не увлекайтесь каскадной инвалидацией — лучше кэшировать мелкие кусочки и инвалидировать точечно, чем кэшировать большие блоки и инвалидировать всё разом.

Мини-разбор реальной ситуации

Маркетплейс кэшировал всю страницу каталога одним ключом. При изменении любого товара кэш сбрасывался целиком, вызывая каскад запросов к БД. Решение: кэшировать каждый товар отдельно (product:42) и собирать каталог из маленьких кэшей. Теперь изменение одного товара инвалидирует только один ключ.

Что запомнить

  • TTL + event-driven инвалидация = надёжная комбинация для большинства случаев
  • Удаляйте (DEL), а не обновляйте (SET) — это безопаснее при конкурентном доступе
  • Кэшируйте мелкие единицы — инвалидация будет точнее и дешевле

Итог

Инвалидация кэша — самая сложная часть кэширования. Комбинация TTL (safety net) + event-driven удаление (точность) покрывает большинство сценариев. Кэшируйте мелко, инвалидируйте точечно, всегда думайте о race conditions.

Комментарии к уроку

Войдите, чтобы оставить комментарий.

Пока нет комментариев — будьте первым.