Как реализовать кеширование в backend-приложении?

«Как реализовать кеширование в backend-приложении?» — вопрос из категории DevOps и инфраструктура, который задают на 33% собеседований Data Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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

1. In-memory кеш приложения (Local Cache): Используется для данных, специфичных для экземпляра приложения или очень горячих. Например, справочники конфигурации.

  • Реализация на Python (используя functools.lru_cache):

    from functools import lru_cache
    import requests
    
    @lru_cache(maxsize=128, ttl=300)  # Кеш на 128 элементов с TTL 5 минут
    def get_country_name(country_code: str) -> str:
        # Дорогой вызов к внешнему API или БД
        response = requests.get(f'https://api.example.com/countries/{country_code}')
        return response.json()['name']

2. Распределенный кеш (Distributed Cache): Критически важен для масштабируемых приложений с несколькими инстансами. Redis — стандартный выбор.

  • Паттерн Cache-Aside (Lazy Loading):

    import redis
    import json
    
    redis_client = redis.Redis(host='redis-cluster.example.com', port=6379, decode_responses=True)
    
    def get_user_profile(user_id: int):
        cache_key = f'user_profile:{user_id}'
    
        # 1. Проверяем кеш
        cached_data = redis_client.get(cache_key)
        if cached_data:
            return json.loads(cached_data)
    
        # 2. Если нет в кеше, идем в БД (например, PostgreSQL)
        user_data = db_session.query(User).filter_by(id=user_id).first()
        if not user_data:
            return None
    
        # 3. Сохраняем в кеш на 1 час
        redis_client.setex(cache_key, 3600, json.dumps(user_data.to_dict()))
        return user_data
  • Инвалидация кеша: При обновлении данных в БД необходимо удалять или обновлять соответствующий ключ в Redis.

    def update_user_profile(user_id: int, new_data):
        # 1. Обновляем основное хранилище
        db_session.query(User).filter_by(id=user_id).update(new_data)
        db_session.commit()
    
        # 2. Инвалидируем кеш
        cache_key = f'user_profile:{user_id}'
        redis_client.delete(cache_key)
        # Или сразу обновляем кеш новыми данными
        # redis_client.setex(cache_key, 3600, json.dumps(new_data))

3. HTTP-кеширование: Для статического контента или редко меняющихся API-ответов.

  • Использование заголовков:

    from flask import make_response
    
    @app.route('/api/catalog')
    def get_catalog():
        data = get_catalog_data()  # Может быть закешировано внутри
        response = make_response(json.dumps(data))
        response.headers['Cache-Control'] = 'public, max-age=300'  # 5 минут в кеше браузера/CDN
        response.headers['ETag'] = generate_etag(data)  # Для валидации
        return response

Выбор стратегии:

  • Cache-Aside (Lazy Loading): Универсален, но возможен cache miss penalty.
  • Write-Through: Данные записываются одновременно в кеш и БД. Сложнее, но обеспечивает согласованность.
  • Write-Behind (Write-Back): Запись идет сначала в кеш, а потом асинхронно в БД. Максимальная скорость записи, но риск потери данных при сбое.

В продакшене мы комбинировали Redis для данных сессий и горячих справочников, а для тяжелых агрегаций использовали CDN или предварительно рассчитанные материализованные представления в БД.