Программа курса

Модуль 02 / Урок 6 из 27

Стратегии кеширования

Теория и практика

Кеш — это копия данных из основного хранилища ради скорости. Главная проблема любого кеша — не скорость, а рассинхронизация с источником. Стратегия кеширования — это ответ на два вопроса: кто читает/пишет кеш и когда он обновляется.

Cache-aside (lazy loading) — основной паттерн

Приложение полностью управляет кешем. Чтение: сначала Redis, при промахе — БД, результат кладётся в кеш. Запись: в БД, кеш инвалидируется (удаляется) — новое значение соберётся при следующем чтении.

def get_user(user_id):
    key = f"user:{user_id}"
    raw = redis.get(key)
    if raw is not None:
        return json.loads(raw)                       # hit
    row = db.query("SELECT * FROM users WHERE id=%s", user_id)   # miss
    redis.set(key, json.dumps(row), ex=3600)
    return row

def update_user(user_id, data):
    db.execute("UPDATE users SET ... WHERE id=%s", data, user_id)
    redis.delete(f"user:{user_id}")                  # инвалидация, не запись!

Инвалидация или запись в кеш при update?

Классический спор. Инвалидация (delete) обычно безопаснее: между UPDATE в БД и записью в кеш другой поток мог прочитать старые данные из БД и закешировать их поверх вашей свежей записи — классическая гонка. Инвалидация сводит риск к окну короткой несвежести. Запись в кеш при update оправдана, когда чтение объекта после записи гарантированно частое и дорогое, а порядок операций контролируете вы.

⚠️ Строгой консистентности cache-aside не даёт по определению: данные в кеше всегда могут быть «на полпрохода старше». Проектируйте под «eventual consistency с TTL гарантированно обновляет кеш».

Read-through / Write-through

В этих паттернах кеш — посредник: приложение общается только с ним, а синхронизацию с БД делает библиотека/сервис кеша.

  • Read-through: промах — кеш сам ходит в БД и заполняется. Удобно в одном месте, но поход в БД всё равно виден пользователю;
  • Write-through: запись идёт в кеш, кеш синхронно пишет в БД. Кеш всегда консистентен, но: двойная запись на каждый update и кешируются даже нечитаемые данные.

В связке с Redis эти паттерны обычно строят вручную в репозитории/DAO-слое приложения.

Write-behind (write-back)

Запись — только в кеш; в БД данные уезжают асинхронно (батчами, воркером). Плюс: экстремально быстрые записи, сглаживание пиков нагрузки на БД. Минус: окно потери данных — падение Redis до сброса = потеря изменений. Обязательные условия: персистентность (модуль 4) и данные, потерю которых переживаете вы (счётчики просмотров, метрики, аналитика).

Что вообще кешировать

  • Hot + read-heavy: высокая частота чтений на одну запись (профили, конфиги, справочники);
  • Дорогие вычисления: агрегаты, результаты тяжёлых SQL, отрендеренные фрагменты страниц;
  • Терпимость к несвежести: если данные должны быть строго свежими всегда — кеш Redis вам не поможет, нужна запись в источник.

Анти-кандидаты: данные со строгими транзакционными гарантиями (балансы денег — только в БД), редко читаемые данные («длинный хвост» забивает память), огромные объекты без необходимости.

Сравнение стратегий

ПаттернКонсистентностьСтоимость записиРиск потериКогда
Cache-asideeventual, окно = TTLнизкая (invalidate)нет90% кешей
Read-througheventualнизкаянетобщий DAO-слой
Write-throughсильная (на чтение кеша)высокаянетважна свежесть
Write-behindeventual, отложеннаяминимальнаяестьсчётчики, метрики

Практика

  1. Реализуйте get_user/update_user из урока и специально воспроизведите гонку: reader спит между SELECT и SET, writer успевает сделать UPDATE+DELETE. Убедитесь, что в кеш легли старые данные, и добавьте TTL как «предохранитель».
  2. Сделайте write-behind счётчик просмотров: INCR в Redis, воркер раз в 10 секунд пишет пачку в SQLite/Postgres и обнуляет дельту.
Мини-квиз

1. Почему при update чаще delete, чем запись нового значения в кеш? — Гонка: между чтением старого из БД и записью в кеш чужой поток может закешировать старьё поверх вашего.

2. TTL нужен даже при идеальной инвалидации? — Да: он страхует от потерянных инвалидаций, багов и сетевых сбоев.

3. Когда write-behind недопустим? — Когда потеря записи = финансовая/юридическая проблема: платежи, заказы, балансы.