← Назад к вопросам

В какой момент нужно кэшировать данные

2.0 Middle🔥 201 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

В какой момент кэшировать данные: стратегия и практика

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

Основные критерии для кэширования

1. Высокая стоимость получения данных

  • Дорогие операции БД (JOIN, агрегация по большим таблицам)
  • Внешние API запросы (сетевая задержка)
  • Сложные вычисления (обработка больших объёмов данных)
  • Парсинг/трансформация данных

2. Высокая частота обращений к одним и тем же данным

  • Одни данные запрашиваются множеством пользователей (публичная информация, справочники)
  • Повторяющиеся запросы в одну сессию
  • Данные, которые редко меняются

3. Допустимость небольшой стараелости (stale data)

  • Справочники, категории, налоги — обновляются редко
  • User profile в рамках сессии
  • Статистика, аналитика — допустима задержка в минуты/часы

Когда НЕ кэшировать

Критические случаи:

  • Финансовые операции (баланс, платежи, транзакции)
  • Состояние аутентификации и разрешений (auth, permissions)
  • Персональные данные, которые часто меняются
  • Данные с требованиями ACID

Уровни кэширования и их применение

1. Кэш на уровне приложения (in-memory)

from functools import lru_cache
import time

# ✅ Простое решение для справочников
@lru_cache(maxsize=128)
def get_tax_rate(country_code: str) -> float:
    # дорогой запрос в БД
    return query_tax_rate(country_code)

# ✅ С явным контролем TTL
class CachedRepository:
    def __init__(self):
        self._cache = {}
        self._timestamps = {}
    
    def get(self, key: str, ttl_seconds: int = 300) -> Optional[Any]:
        if key in self._cache:
            age = time.time() - self._timestamps[key]
            if age < ttl_seconds:
                return self._cache[key]
            del self._cache[key]
        
        value = self._fetch_from_source(key)
        self._cache[key] = value
        self._timestamps[key] = time.time()
        return value

2. Кэш на уровне БД (Redis, Memcached)

# ✅ Использование Redis для распределённого кэша
import redis
from json import dumps, loads

class RedisCache:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client
    
    def get_user_profile(self, user_id: int, ttl: int = 3600):
        cache_key = f"user:{user_id}:profile"
        
        # Попытка получить из кэша
        cached = self.redis.get(cache_key)
        if cached:
            return loads(cached)
        
        # Если нет — запрос в БД
        user = db.query(User).get(user_id)
        
        # Сохраняем в кэш
        self.redis.setex(cache_key, ttl, dumps(user.to_dict()))
        
        return user.to_dict()

3. Кэш на уровне HTTP

from fastapi import FastAPI
from fastapi.responses import Response

app = FastAPI()

# ✅ Кэширование публичных данных
@app.get("/api/categories")
async def get_categories(response: Response):
    response.headers["Cache-Control"] = "public, max-age=3600"
    return await db.fetch_categories()

Практический пример: слои кэширования

class DataService:
    def __init__(self, db, redis_client):
        self.db = db
        self.redis = redis_client
        self._memory_cache = {}  # L1: In-memory
    
    async def get_product(self, product_id: int) -> Product:
        # L1: Memory cache (очень быстро, но локально)
        if product_id in self._memory_cache:
            return self._memory_cache[product_id]
        
        # L2: Redis (быстро, распределённо)
        cache_key = f"product:{product_id}"
        cached = await self.redis.get(cache_key)
        if cached:
            product = Product.from_dict(loads(cached))
            self._memory_cache[product_id] = product  # кэшируем в памяти
            return product
        
        # L3: БД (медленно)
        product = await self.db.get_product(product_id)
        
        # Заполняем обратно все слои
        await self.redis.setex(cache_key, 3600, dumps(product.to_dict()))
        self._memory_cache[product_id] = product
        
        return product

Инвалидация кэша

Когда инвалидировать:

  • При создании/обновлении/удалении данных
  • По расписанию (TTL)
  • На основе событий
# ✅ Инвалидация при обновлении
async def update_product(self, product_id: int, data: dict):
    # Обновляем в БД
    product = await self.db.update_product(product_id, data)
    
    # Инвалидируем кэш
    await self.redis.delete(f"product:{product_id}")
    if product_id in self._memory_cache:
        del self._memory_cache[product_id]
    
    return product

# ✅ Soft invalidation (запрос на свежесть)
async def get_with_validation(self, product_id: int) -> Product:
    cached = await self.redis.get(f"product:{product_id}")
    if cached:
        # Проверяем в фоне, нужны ли обновления
        asyncio.create_task(self._validate_product(product_id))
        return Product.from_dict(loads(cached))
    return await self.get_product(product_id)

Резюме

Кэшируй когда:

  • Операция дорогая (БД, внешние API, вычисления)
  • Частые повторяющиеся обращения
  • Допустима небольшая стараелость данных

Не кэшируй:

  • Критические данные, требующие актуальности
  • Редко запрашиваемые данные (потеря памяти)
  • Данные с высокими требованиями к консистентности

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

В какой момент нужно кэшировать данные | PrepBro