Паттерн cache-aside
Cache-aside — самый распространённый паттерн кэширования. Если вы один раз поймёте его схему, вы поймёте 80% кэширования вообще.
Идея проста: сначала спроси у кэша. Нет данных — сходи в БД и положи их в кэш. В следующий раз кэш ответит сам.
Cache-aside (его ещё называют «lazy loading», ленивая загрузка) — стратегия, при которой приложение само управляет кэшем. Кэш заполняется «по требованию»: только тем, что реально запрашивают. Это рабочая лошадка для большинства read-heavy сценариев.
Как это работает
Запрос данных при cache-aside
Приложение
|
| 1. GET cache:user:42
v
+--------+ попадание (hit)
| Redis | -----------------------> вернуть данные
+--------+
| промах (miss)
| 2. SELECT ... FROM users
v
+--------+
| БД | -----------------------> данные
+--------+
|
| 3. SET cache:user:42 ... EX 300
v
положить в кэш и вернуть приложению
Шаги: (1) приложение спрашивает кэш; (2) при промахе идёт в БД; (3) кладёт результат в кэш с TTL и возвращает. При следующем запросе данные уже в кэше — БД не тревожат.
Команды в основе
# Попытка достать из кэша
GET cache:user:42
# (nil) -- промах, идём в БД, затем:
SET cache:user:42 "{...данные...}" EX 300
# Следующий GET cache:user:42 вернёт данные сразу
Демонстрация: cache-aside на Python
Смоделируем кэш поверх «медленной БД» и увидим выигрыш на повторных запросах:
import time
# "Медленная БД" — имитируем задержку
def slow_db_query(user_id):
time.sleep(0.05) # будто запрос к БД занял время
return {"id": user_id, "name": f"User{user_id}"}
cache = {} # это наш Redis
db_calls = 0
def get_user(user_id):
global db_calls
key = f"cache:user:{user_id}"
if key in cache: # 1. спрашиваем кэш
return cache[key], "HIT"
db_calls += 1 # 2. промах -> идём в БД
data = slow_db_query(user_id)
cache[key] = data # 3. кладём в кэш
return data, "MISS"
# Три запроса к одному пользователю
for i in range(3):
data, status = get_user(42)
print(f"Запрос {i+1}: {status} -> {data}")
print(f"\nОбращений к БД: {db_calls} (а запросов было 3)")
print("Cache-aside: БД потревожена один раз, дальше отвечает кэш.")
Первый запрос — промах и поход в БД. Два следующих — попадания, мгновенные и без нагрузки на БД. В этом вся суть.
Как работает под капотом
В cache-aside кэш и БД не связаны напрямую — синхронизацию полностью контролирует приложение. Кэш не знает о существовании БД. Это даёт гибкость (кэшируем что угодно и как угодно), но накладывает ответственность: приложение должно правильно инвалидировать устаревшие данные. TTL — главный инструмент: даже если вы забыли что-то обновить, через срок жизни ключ исчезнет и данные перечитаются.
Частые ошибки
- Кэш без TTL. Данные устареют и «застрянут» в кэше навсегда.
- Кэширование промахов не предусмотрено. Если данных нет и в БД, каждый запрос будет ходить в БД (см. урок про защиту кэша).
- Несогласованность при записи. Обновили БД, но забыли инвалидировать кэш — пользователь видит старое.
Best practices
- Всегда задавайте TTL — это «страховка» от застаревших данных.
- При обновлении данных инвалидируйте (удаляйте) соответствующий кэш-ключ.
- Кэшируйте то, что часто читают и редко меняют, — выигрыш максимален.
Итог: Cache-aside — приложение спрашивает кэш, при промахе идёт в БД и заполняет кэш. Простой, гибкий, самый частый паттерн. Его слабые места — инвалидация и застаревание — лечатся грамотным TTL.
Инвалидация: ахиллесова пята кэша
Есть известная шутка, что в программировании всего две сложные вещи: инвалидация кэша и придумывание имён. Cache-aside перекладывает инвалидацию на ваше приложение, поэтому стоит сразу выбрать стратегию, как держать кэш свежим.
- По TTL. Самый простой путь: данные живут заданное время и сами протухают. Подходит, когда небольшая задержка свежести допустима (каталог, статистика).
- Явная инвалидация. При изменении данных приложение удаляет соответствующий кэш-ключ через
DEL. Свежесть мгновенная, но нужно не забыть удалить ключ во всех местах, где данные меняются. - Версионирование ключа. В имя ключа включают версию или метку времени изменения; при обновлении версия растёт, и старый ключ просто перестаёт запрашиваться (а потом вытесняется).
На практике эти подходы комбинируют: явная инвалидация при записи плюс TTL как страховка на случай, если какой-то путь обновления данных её пропустил. Такой «ремень и подтяжки» — разумный дефолт для большинства веб-приложений, где данные читают намного чаще, чем меняют.