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

Какие плюсы и минусы у использования кеширования?

2.0 Middle🔥 251 комментариев
#REST API и HTTP#Архитектура и паттерны

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

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

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

Кеширование: плюсы и минусы

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

ПЛЮСЫ

1. Значительное улучшение производительности

Кеш намного быстрее чем источник данных.

Без кеша:
[Запрос] → [БД] (10-100ms) → [Ответ]

С кешем:
[Запрос] → [Redis] (1-5ms) → [Ответ]

В памяти доступ в 10-100 раз быстрее.

import time
from functools import lru_cache

def slow_function(n):
    time.sleep(1)  # 1 секунда запроса
    return n * 2

# Первый вызов — 1 секунда
result = slow_function(5)

# Второй вызов — 1 секунда (без кеша)
result = slow_function(5)

# С кешем
@lru_cache(maxsize=128)
def cached_function(n):
    time.sleep(1)
    return n * 2

# Первый вызов — 1 секунда
result = cached_function(5)

# Второй вызов — 0.0001 секунд (из кеша)
result = cached_function(5)

2. Снижение нагрузки на БД

Без кеша: каждый запрос → БД
[Client 1] ─┐
[Client 2] ─┼─→ [БД] (перегруз)
[Client 3] ─┘

С кешем:
[Client 1] ─┐
[Client 2] ─┼─→ [Redis Cache] → [БД] (1 запрос)
[Client 3] ─┘

Вместо 1000 запросов в БД может быть 10.

# Django
from django.views.decorators.cache import cache_page

@cache_page(60 * 5)  # Кеш на 5 минут
def expensive_view(request):
    users = User.objects.all()  # Один запрос в БД на 5 минут
    return render(request, 'users.html', {'users': users})

3. Экономия трафика

Не нужно передавать данные снова по сети.

# API
from django.views.decorators.cache import cache_page

@cache_page(300)  # 5 минут
@api_view(['GET'])
def get_products(request):
    products = Product.objects.all()
    return Response(ProductSerializer(products, many=True).data)

Первый клиент → полный ответ → сохранён в кеше. Остальные клиенты получают ответ из кеша (без сетевого запроса).

4. Улучшение UX

Быстрые ответы = счастливые пользователи.

# Вместо ожидания 5 секунд, пользователь получает ответ за 50ms
response_time_without_cache = 5000  # 5 секунд
response_time_with_cache = 50       # 50 миллисекунд

5. Масштабируемость

Кеш позволяет обслужить больше пользователей с тем же оборудованием.

Без кеша: 100 concurrent пользователей → нужна мощная БД

С кешем:
100 concurrent пользователей
  → Cache hit rate 80%
  → Только 20 запросов в БД
  → Нужна меньше мощная БД

МИНУСЫ

1. Проблема свежести данных (Cache Invalidation)

Кеш может содержать устаревшие данные.

# Пользователь обновил профиль
user = User.objects.get(id=1)
user.name = "John Doe"
user.save()

# Но кеш всё ещё содержит старое имя!
cached_user = cache.get(f"user:{1}")  # Старое: "John"

# Нужно инвалидировать кеш
cache.delete(f"user:{1}")

Проблемы:

  • Трудно отследить когда нужна инвалидация
  • Race conditions при одновременных обновлениях
  • "There are only two hard things in Computer Science: cache invalidation and naming things" (Фил Карлтон)

2. Потребление памяти

Кеш занимает память.

import sys

data = [i for i in range(1000000)]
print(f"Memory: {sys.getsizeof(data)} bytes")

# Redis в памяти
# 1GB кеша → 1GB памяти занято
# Если кеш большой, нужна дополнительная оперативная память

3. Сложность архитектуры

Простая архитектура (без кеша):
[Клиент] → [Сервер] → [БД]

Сложная архитектура (с кешем):
[Клиент] → [Сервер] → [Redis Cache] → [БД]
                    ↑
            (нужна синхронизация)

Нужно:

  • Развернуть Redis
  • Мониторить Redis
  • Очищать кеш при обновлениях
  • Обрабатывать случаи когда Redis недоступен

4. Потенциальные баги

Cache Penetration (проникновение в кеш)

# Ищем пользователя ID = 99999 (не существует)
user = cache.get("user:99999")  # None

if user is None:
    user = User.objects.filter(id=99999).first()  # None из БД
    # Не сохраняем в кеш!

# Следующий запрос:
# Снова ищет в БД (кеш прозрачен)

Решение: Кешировать None-значения с коротким TTL.

user = cache.get(f"user:{99999}")
if user is None:
    user = User.objects.filter(id=99999).first()
    cache.set(f"user:{99999}", user, timeout=60)  # Кешируем None

Cache Stampede (толпа к кешу)

Популярный ключ кеша (key="homepage")
Кеш истёк (TTL=0)
Приходит 1000 запросов одновременно

Все 1000 запросов идят в БД!

Решение: Distributed locking.

import redis
from redis import Redis

redis_client = Redis()

def get_popular_data():
    cache_key = "homepage"
    data = redis_client.get(cache_key)
    
    if data is None:
        # Возьмём lock перед обновлением
        lock = redis_client.lock(f"{cache_key}:lock", timeout=10)
        
        if lock.acquire(blocking=False):  # Non-blocking
            try:
                # Только один поток выполняет запрос
                data = fetch_from_db()
                redis_client.setex(cache_key, 300, data)
            finally:
                lock.release()
        else:
            # Ждём пока другой поток обновит кеш
            data = fetch_from_db()
    
    return data

Cache Avalanche (лавина кеша)

Много ключей кеша истекают одновременно
  ↓
Много запросов идят в БД
  ↓
БД перегруженa
  ↓
Приложение падает

Решение: Использовать разные TTL.

# Плохо — все истекают в одно время
cache.set("key1", value1, timeout=3600)
cache.set("key2", value2, timeout=3600)
cache.set("key3", value3, timeout=3600)

# Хорошо — разные TTL
import random

ttl = 3600 + random.randint(-300, 300)  # 3300-3900 секунд
cache.set("key1", value1, timeout=ttl)

5. Трудность тестирования

def get_user(id):
    # Есть ли кеш? Нет способа узнать в тесте
    cached = cache.get(f"user:{id}")
    if cached:
        return cached
    
    user = User.objects.get(id=id)
    cache.set(f"user:{id}", user, timeout=300)
    return user

# Тест
def test_get_user():
    user = get_user(1)
    assert user.name == "John"
    
    # Обновляем пользователя
    user.name = "Jane"
    user.save()
    
    # Получаем снова — кеш вернёт старое!
    user2 = get_user(1)
    assert user2.name == "Jane"  # FAIL (кеш вернул "John")

Решение: Отключить кеш в тестах.

from unittest.mock import patch

@patch('myapp.cache.get')
@patch('myapp.cache.set')
def test_get_user(mock_cache_set, mock_cache_get):
    mock_cache_get.return_value = None  # Кеш всегда "пуст"
    
    user = get_user(1)
    assert user.name == "John"

6. Усложнение отладки

# Плохо: где данные приходят?
result = get_user(1)  # Из кеша? Из БД? Неизвестно

# Логирование помогает
def get_user(id):
    cached = cache.get(f"user:{id}")
    if cached:
        logger.info(f"Cache hit for user:{id}")
        return cached
    
    logger.info(f"Cache miss for user:{id}, fetching from DB")
    user = User.objects.get(id=id)
    cache.set(f"user:{id}", user)
    return user

Когда использовать кеширование?

✅ Используй когда:

  • Данные часто читаются, редко обновляются
  • Доступ к данным медленный (БД, внешний API)
  • Hit rate выше 50% (половина запросов из кеша)
  • Немного устаревшие данные приемлемы

❌ Избегай когда:

  • Данные постоянно обновляются
  • Нужна свежесть данных в реальном времени
  • Кеш-инвалидация слишком сложная
  • Разовые запросы (никакого повторного использования)

Стратегии кеширования

# 1. Time-based (TTL)
cache.set("key", value, timeout=300)  # 5 минут

# 2. Event-based (инвалидация при событии)
def update_user(user):
    user.save()
    cache.delete(f"user:{user.id}")

# 3. LRU (удалять старые если полно)
from functools import lru_cache
@lru_cache(maxsize=128)  # Хранить последние 128

# 4. Write-through (обновлять кеш всегда)
def create_user(name):
    user = User.objects.create(name=name)
    cache.set(f"user:{user.id}", user)

# 5. Write-behind (асинхронно обновлять)
from celery import shared_task

@shared_task
def update_cache():
    for user in User.objects.all():
        cache.set(f"user:{user.id}", user)

Итог

Кеширование — мощный инструмент для оптимизации. Но используй с умом:

  • Профилируй перед кешированием
  • Убедись что hit rate окупает сложность
  • Внимательно проектируй инвалидацию
  • Мониторь кеш (hit/miss ratio, memory usage)

Часто "на память" лучше всего решение — оптимизировать запросы чем добавлять кеш.

Какие плюсы и минусы у использования кеширования? | PrepBro