Redis: кеш, который не превращается в базу

Cache-aside, инвалидация, вытеснение — и лавина запросов, когда популярный ключ истёк.

Базовый паттерн: cache-aside

Приложение сначала спрашивает кеш, при промахе идёт в базу и кладёт результат в кеш с TTL:

value = redis.get(key)
if value is None:                 # промах
    value = db.query(...)
    redis.set(key, value, ex=300) # TTL обязателен
return value

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

Инвалидация

Знаменитая «одна из двух сложных проблем» на практике сводится к выбору:

Практичный дефолт: TTL везде + явный DEL там, где устаревание заметно пользователю.

Память и вытеснение

Redis живёт в RAM, поэтому лимит и политика вытеснения должны быть заданы явно:

maxmemory 2gb
maxmemory-policy allkeys-lru

Для чистого кеша allkeys-lru — правильный выбор: при нехватке памяти вытесняются давно не читанные ключи. Дефолтная политика noeviction для кеша опасна: память кончилась — Redis начинает отклонять записи, и кеш тихо перестаёт обновляться.

Лавина (cache stampede)

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

Защиты, от простой к сложной:

  1. джиттер TTL300 + random(0, 60) секунд, чтобы ключи не истекали синхронно;
  2. лок на пересчёт — только один запрос перезаполняет ключ, остальные ждут или отдают старое значение;
  3. фоновое обновление горячих ключей до истечения.

Кеш — не хранилище

Всё в Redis должно быть восстановимо из базы. Если потеря инстанса Redis теряет данные (корзины, сессии единственного экземпляра, счётчики) — это уже не кеш, и у такого Redis должны быть persistence (AOF), реплика и бэкапы — со всеми обязанностями настоящей базы.