Средний

Redis как кэш: паттерны и практики

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

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

Redis как кэш: паттерны и практики

Redis — in-memory хранилище, которое чаще всего используется как кэш. Cache-aside, write-through, cache warming — основные паттерны, которые определяют, когда и как данные попадают в кэш.

Почему это важно: Redis даёт время ответа < 1 мс для любого объёма кэшированных данных. Но выбор неправильного паттерна кэширования приводит к stale data, thundering herd или избыточному потреблению памяти.

Главная идея

Cache-aside (lazy loading) — самый популярный паттерн: приложение сначала проверяет кэш, при промахе читает из БД и сохраняет в кэш. Write-through дополнительно обновляет кэш при каждой записи.

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

  1. Приложение получает запрос: GET /users/42/profile
  2. Cache-aside: проверяем Redis по ключу user:42:profile
  3. Cache miss: читаем из PostgreSQL, сериализуем в JSON
  4. Сохраняем в Redis с SET user:42:profile EX 600 (TTL 10 мин)
  5. Следующие запросы на этот профиль — из Redis за < 1 мс
  6. Пользователь обновляет профиль → DELETE user:42:profile из Redis
  7. Следующий GET снова промахнётся и обновит кэш свежими данными

Примеры кода

Cache-aside паттерн

class UserService
  def profile(user_id)
    cache_key = "user:\#{user_id}:profile"

    # 1. Попытка чтения из кэша
    cached = REDIS.get(cache_key)
    return JSON.parse(cached) if cached

    # 2. Cache miss — чтение из БД
    user = User.includes(:settings, :avatar).find(user_id)
    data = UserSerializer.new(user).as_json

    # 3. Сохранение в кэш с TTL
    REDIS.setex(cache_key, 600, data.to_json)
    data
  end

  def update_profile(user_id, params)
    user = User.find(user_id)
    user.update!(params)

    # Инвалидация кэша при изменении
    REDIS.del("user:\#{user_id}:profile")
  end
end

Защита от thundering herd

class CacheService
  # Mutex-lock: только один процесс обновляет кэш
  def fetch_with_lock(key, ttl: 300, lock_ttl: 10)
    cached = REDIS.get(key)
    return JSON.parse(cached) if cached

    lock_key = "lock:\#{key}"
    # Пытаемся захватить блокировку (SET NX EX)
    if REDIS.set(lock_key, '1', nx: true, ex: lock_ttl)
      begin
        data = yield  # выполняем дорогую операцию
        REDIS.setex(key, ttl, data.to_json)
        data
      ensure
        REDIS.del(lock_key)
      end
    else
      # Другой процесс обновляет — ждём и пробуем снова
      sleep(0.1)
      cached = REDIS.get(key)
      cached ? JSON.parse(cached) : yield
    end
  end
end

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

  • Redis хранит данные в RAM — поэтому он быстр, но ограничен объёмом памяти
  • maxmemory-policy: allkeys-lru — при нехватке памяти Redis удаляет наименее используемые ключи
  • SETEX (SET + EX) — атомарная операция: запись + установка TTL одной командой
  • SET NX (Not eXists) — записать только если ключа нет, основа для распределённых блокировок
  • Pipeline и MGET/MSET — батч-операции для снижения latency при множественных запросах

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

  • «Redis вечно хранит данные» — по умолчанию Redis in-memory, без персистенции данные теряются при рестарте
  • «Нужно кэшировать всё» — кэшируйте только то, что читается чаще, чем обновляется
  • «Инвалидация по TTL достаточна» — для критичных данных нужна активная инвалидация при изменении

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

  • Cache-aside: read → check cache → miss → read DB → write cache. Самый гибкий паттерн
  • Инвалидация: delete при записи (не update) — проще и надёжнее
  • Защита от thundering herd: distributed lock через SET NX или stale-while-revalidate

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

{:term=>"Cache-aside", :definition=>"Паттерн, при котором приложение само управляет кэшем: проверяет при чтении, инвалидирует при записи"}
{:term=>"Thundering herd", :definition=>"Ситуация, когда при инвалидации кэша множество запросов одновременно нагружают БД"}
{:term=>"SETEX", :definition=>"Redis-команда: записать значение с автоматическим TTL одной атомарной операцией"}

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

Cache-aside с TTL + активной инвалидацией покрывает 90% сценариев. Не усложняйте: начните с простого кэша, добавляйте lock и warming только когда видите thundering herd или cold start в метриках.

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

Социальная сеть кэшировала ленту пользователя в Redis. При публикации нового поста инвалидировались кэши всех подписчиков (100 000+ ключей). Это вызывало thundering herd на БД. Решение: stale-while-revalidate — отдавать старую ленту из кэша, пока фоновая задача обновляет кэш.

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

  • Cache-aside: check cache → miss → read DB → write cache → return
  • При записи — удаляй (DEL), не обновляй (SET) — это проще и безопаснее
  • Для горячих ключей — используй lock или stale-while-revalidate

Итог

Redis — стандартный выбор для серверного кэша. Паттерн cache-aside с TTL и активной инвалидацией при записи — базовая стратегия. Для высоконагруженных сценариев добавляйте distributed lock и cache warming.

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

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

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