Подписывайтесь:

Блог AST-SoftPro

Кэширование и производительность: Redis, CDN, стратегии инвалидации

05.07.2026 12 мин чтения
Кэширование и производительность: 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}")

Частые ошибки

  1. Кэширование без инвалидации. Данные в кэше устаревают, но никто их не удаляет. Всегда используйте TTL или инвалидацию при записи.

  2. Кэширование персональных данных. Данные профиля пользователя не должны кэшироваться в общем кэше — разные пользователи увидят данные друг друга.

  3. Отсутствие метрик. Без hit rate невозможно понять, работает ли кэш эффективно.

  4. Слишком короткий TTL. TTL в 1 секунду не даёт выигрыша — данные перезагружаются из базы слишком часто.

  5. Слишком длинный TTL. TTL в 1 час для часто меняющихся данных приводит к тому, что пользователи видят устаревшую информацию.

Заключение

Кэширование — это не один инструмент, а набор стратегий на разных уровнях. Правильный кэш снижает нагрузку на базу данных и ускоряет отклик приложения. Начните с простого — Redis с TTL — и усложняйте по мере необходимости.

Ключевые моменты:

  1. Кэширование работает на уровнях: CDN → приложение (Redis) → база данных.

  2. Cache-Aside — самый простой и распространённый паттерн.

  3. Инвалидация — самая сложная часть: используйте TTL + инвалидацию при записи.

  4. CDN кэширует статику и публичные API-ответы, но не персональные данные.

  5. Мониторьте hit rate — без метрик невозможно оценить эффективность кэша.

AI-Помощник