Кеш — это копия данных из основного хранилища ради скорости. Главная проблема любого кеша — не скорость, а рассинхронизация с источником. Стратегия кеширования — это ответ на два вопроса: кто читает/пишет кеш и когда он обновляется.
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-aside | eventual, окно = TTL | низкая (invalidate) | нет | 90% кешей |
| Read-through | eventual | низкая | нет | общий DAO-слой |
| Write-through | сильная (на чтение кеша) | высокая | нет | важна свежесть |
| Write-behind | eventual, отложенная | минимальная | есть | счётчики, метрики |
Практика
- Реализуйте get_user/update_user из урока и специально воспроизведите гонку: reader спит между SELECT и SET, writer успевает сделать UPDATE+DELETE. Убедитесь, что в кеш легли старые данные, и добавьте TTL как «предохранитель».
- Сделайте write-behind счётчик просмотров: INCR в Redis, воркер раз в 10 секунд пишет пачку в SQLite/Postgres и обнуляет дельту.
Мини-квиз
1. Почему при update чаще delete, чем запись нового значения в кеш? — Гонка: между чтением старого из БД и записью в кеш чужой поток может закешировать старьё поверх вашего.
2. TTL нужен даже при идеальной инвалидации? — Да: он страхует от потерянных инвалидаций, багов и сетевых сбоев.
3. Когда write-behind недопустим? — Когда потеря записи = финансовая/юридическая проблема: платежи, заказы, балансы.