Redis как кэш: паттерны и практики
Redis — in-memory хранилище, которое чаще всего используется как кэш. Cache-aside, write-through, cache warming — основные паттерны, которые определяют, когда и как данные попадают в кэш.
Почему это важно: Redis даёт время ответа < 1 мс для любого объёма кэшированных данных. Но выбор неправильного паттерна кэширования приводит к stale data, thundering herd или избыточному потреблению памяти.
Главная идея
Cache-aside (lazy loading) — самый популярный паттерн: приложение сначала проверяет кэш, при промахе читает из БД и сохраняет в кэш. Write-through дополнительно обновляет кэш при каждой записи.
Как это выглядит на практике
- Приложение получает запрос: GET /users/42/profile
- Cache-aside: проверяем Redis по ключу user:42:profile
- Cache miss: читаем из PostgreSQL, сериализуем в JSON
- Сохраняем в Redis с SET user:42:profile EX 600 (TTL 10 мин)
- Следующие запросы на этот профиль — из Redis за < 1 мс
- Пользователь обновляет профиль → DELETE user:42:profile из Redis
- Следующий 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
Термины урока
Связь с работой 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.
Комментарии к уроку
Войдите, чтобы оставить комментарий.
Пока нет комментариев — будьте первым.