Блог AST-SoftPro
Кэширование и производительность: Redis, CDN, стратегии инвалидации
Кэширование и производительность: Redis, CDN, стратегии инвалидации
Кэширование — самый эффективный способ ускорить приложение. Правильно настроенный кэш может снизить нагрузку на базу данных в 10-100 раз и сократить время ответа с секунд до миллисекунд. В этой статье разберём уровни кэширования, стратегии инвалидации и практические примеры на Python.
Уровни кэширования
Кэширование работает на разных уровнях — от браузера до базы данных. Каждый уровень решает свою задачу.
L1: Кэш браузера и CDN
CDN (Content Delivery Network) хранит статические ресурсы (изображения, CSS, JavaScript) на серверах, близких к пользователю. Это снижает задержку и разгружает основной сервер.
L2: Кэш приложения (Redis, Memcached)
Кэш в памяти, доступный всем экземплярам приложения. Используется для кэширования результатов запросов, сессий, очередей.
L3: Кэш базы данных (Buffer Pool)
PostgreSQL и MySQL имеют встроенные буферы для кэширования часто используемых страниц данных.
Redis: универсальный кэш
Redis — в-memory хранилище, которое используется не только как кэш, но и как брокер сообщений, хранилище сессий и система очередей.
Базовое кэширование (Cache-Aside)
Самый распространённый паттерн: сначала проверяем кэш, если данных нет — загружаем из базы и сохраняем в кэш.
import redis
import json
r = redis.Redis(host="redis", port=6379, db=0, decode_responses=True)
def get_product(product_id: int, db_session):
"""Cache-Aside: сначала кэш, потом база"""
cache_key = f"product:{product_id}"
# Попытка чтения из кэша
cached = r.get(cache_key)
if cached:
return json.loads(cached)
# Пропуск — чтение из базы
product = db_session.query(Product).get(product_id)
if not product:
return None
# Сохранение в кэш на 5 минут
r.setex(cache_key, 300, json.dumps({
"id": product.id,
"name": product.name,
"price": product.price,
}))
return product
def update_product(product_id: int, data: dict, db_session):
"""При обновлении — инвалидация кэша"""
product = db_session.query(Product).get(product_id)
product.name = data.get("name", product.name)
product.price = data.get("price", product.price)
db_session.commit()
# Удаление из кэша
r.delete(f"product:{product_id}")
Запись через кэш
При записи данные сначала сохраняются в кэш, а затем асинхронно в базу. Подходит для данных, которые часто читаются и редко обновляются.
def set_product_through(product_id: int, data: dict):
"""Write-Through: кэш первичен, база — вторично"""
cache_key = f"product:{product_id}"
r.setex(cache_key, 3600, json.dumps(data))
# Асинхронная запись в базу (через Celery)
save_to_db.delay(product_id, data)
Устаревшие данные с фоновым обновлением
Кэш отдаёт устаревшие данные, пока фоновый процесс обновляет их из базы. Это обеспечивает минимальную задержку для пользователя.
import threading
def get_product_stale(product_id: int, db_session):
"""Stale-While-Revalidate"""
cache_key = f"product:{product_id}"
stale_key = f"product:{product_id}:stale"
# Читаем из кэша (даже если устарел)
cached = r.get(cache_key)
if cached:
# Если есть флаг устаревания — запускаем обновление в фоне
if r.exists(stale_key):
refresh_cache.delay(product_id)
return json.loads(cached)
# Пропуск — читаем из базы
product = db_session.query(Product).get(product_id)
r.setex(cache_key, 300, json.dumps({
"id": product.id,
"name": product.name,
"price": product.price,
}))
return product
Стратегии инвалидации кэша
Инвалидация — удаление устаревших данных из кэша. Это самая сложная часть кэширования.
TTL (время жизни)
Самый простой подход: данные живут ограниченное время. После истечения TTL кэш перезагружается из базы.
# Кэш на 5 минут для каталога
r.setex("products:catalog", 300, json.dumps(products))
# Кэш на 1 час для статических данных
r.setex("config:site", 3600, json.dumps(config))
Плюсы: простота, предсказуемость. Минусы: данные могут быть устаревшими до истечения TTL.
Инвалидация при записи
При обновлении данных в базе — немедленное удаление соответствующего ключа из кэша.
# При обновлении товара
r.delete(f"product:{product_id}")
r.delete("products:catalog") # Удаляем кэш каталога
Плюсы: актуальные данные. Минусы: нужно отслеживать все зависимости (обновление товара → инвалидация каталога).
Cache Invalidation Patterns
| Паттерн | Когда использовать | Пример |
|---|---|---|
| TTL | Редко меняющиеся данные | Конфигурация сайта |
| Write-Invalidate | Критичная актуальность | Профиль пользователя |
| Запись через кэш | Частые чтения, редкие записи | Каталог товаров |
| Lazy Loading | Не все данные нужны сразу | Страница товара |
CDN: кэширование на границе сети
CDN кэширует статические ресурсы на серверах, близких к пользователям. Это снижает задержку и разгружает основной сервер.
Настройка кэширования через HTTP-заголовки:
from fastapi import FastAPI, Response
from fastapi.staticfiles import StaticFiles
app = FastAPI()
# Статические файлы с кэшированием
app.mount("/static", StaticFiles(directory="static"), name="static")
@app.get("/api/products")
def get_products(response: Response):
products = get_products_from_db()
# Кэширование на CDN — 1 час
response.headers["Cache-Control"] = "public, max-age=3600, s-maxage=7200"
response.headers["ETag"] = '"v1.2.3"'
return products
@app.get("/api/user/profile")
def get_profile(response: Response):
profile = get_user_profile()
# Не кэшировать — персональные данные
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
return profile
Когда CDN кэшировать, а когда нет:
| Ресурс | Кэшировать? | TTL |
|---|---|---|
| CSS, JS, изображения | Да | 1 год (с хэшем в имени) |
| HTML-страницы | Да | 1-5 минут |
| API-ответы (каталог) | Да | 1 час |
| API-ответы (профиль) | Нет | — |
| API-ответы (заказы) | Нет | — |
Практика: кэширование в высоконагруженном приложении
Рассмотрим пример интернет-магазина с 10 000 активных пользователей.
Архитектура кэширования:
[Браузер] ──CDN──> [Nginx] ──App Cache──> [Redis] ──> [PostgreSQL]
│ │ │
│ └── Static files └── Sessions
│
└── Browser Cache (1 год для static)
Реализация multi-level кэша:
import redis
import json
from functools import wraps
class CacheManager:
def __init__(self, redis_client: redis.Redis):
self.r = redis_client
def get(self, key: str):
"""Чтение из кэша"""
data = self.r.get(key)
if data:
return json.loads(data)
return None
def set(self, key: str, value, ttl: int = 300):
"""Запись в кэш"""
self.r.setex(key, ttl, json.dumps(value))
def invalidate(self, key: str):
"""Инвалидация по шаблону"""
keys = self.r.keys(key)
if keys:
self.r.delete(*keys)
def cached(ttl: int = 300):
"""Декоратор для кэширования функций"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
cache = CacheManager(redis.Redis())
key = f"func:{func.__name__}:{hash(str(args) + str(kwargs))}"
result = cache.get(key)
if result is not None:
return result
result = func(*args, **kwargs)
cache.set(key, result, ttl)
return result
return wrapper
return decorator
# Использование
cache = CacheManager(redis.Redis())
@cache.cached(ttl=600)
def get_product_catalog(category: str = None):
"""Кэшированный запрос каталога"""
query = Product.query
if category:
query = query.filter_by(category=category)
return [p.to_dict() for p in query.all()]
Метрики кэширования
Без метрик невозможно оценить эффективность кэширования. Основные метрики:
-
Hit Rate — процент запросов, обслуженных из кэша. Целевой показатель: > 90%.
-
Miss Rate — процент пропусков. Если > 10% — нужно пересмотреть стратегию.
-
Eviction Rate — процент вытесненных ключей. Если высокий — увеличьте размер кэша.
-
Memory Usage — использование памяти Redis. Если > 80% — нужно масштабирование.
# Мониторинг hit rate в Redis
hit_rate = r.info("stats")["hit_rate"]
print(f"Cache hit rate: {hit_rate:.1%}")
# Мониторинг использования памяти
memory = r.info("memory")["used_memory_human"]
print(f"Redis memory: {memory}")
Частые ошибки
-
Кэширование без инвалидации. Данные в кэше устаревают, но никто их не удаляет. Всегда используйте TTL или инвалидацию при записи.
-
Кэширование персональных данных. Данные профиля пользователя не должны кэшироваться в общем кэше — разные пользователи увидят данные друг друга.
-
Отсутствие метрик. Без hit rate невозможно понять, работает ли кэш эффективно.
-
Слишком короткий TTL. TTL в 1 секунду не даёт выигрыша — данные перезагружаются из базы слишком часто.
-
Слишком длинный TTL. TTL в 1 час для часто меняющихся данных приводит к тому, что пользователи видят устаревшую информацию.
Заключение
Кэширование — это не один инструмент, а набор стратегий на разных уровнях. Правильный кэш снижает нагрузку на базу данных и ускоряет отклик приложения. Начните с простого — Redis с TTL — и усложняйте по мере необходимости.
Ключевые моменты:
-
Кэширование работает на уровнях: CDN → приложение (Redis) → база данных.
-
Cache-Aside — самый простой и распространённый паттерн.
-
Инвалидация — самая сложная часть: используйте TTL + инвалидацию при записи.
-
CDN кэширует статику и публичные API-ответы, но не персональные данные.
-
Мониторьте hit rate — без метрик невозможно оценить эффективность кэша.