
Кэширование — это способ отвечать быстрее и дешевле, отдавая заранее подготовленный результат вместо повторных дорогих вычислений. Вы выигрываете сразу по трём направлениям:
В типичном проекте кэш даёт 3–10-кратный прирост производительности на популярных страницах и снижает нагрузку на базу на 50–90%.
Кэш можно включать на нескольких уровнях:
Правило: кэшируйте то, что дорого считать и часто спрашивают, но редко меняется или допускает задержку обновления.
Ниже рабочий пример сервиса, который выдаёт карточку товара. Мы кэшируем готовый JSON в Redis c TTL, используем распределённый замок, чтобы один запрос обновлял кэш, а остальные ждали или получали устаревшие данные в пределах короткого окна.
Подготовка:
# Запускаем Redis локально
docker run --rm -p 6379:6379 redis:7
# Установка зависимостей
pip install fastapi uvicorn redis[async] pydantic httpx
Код приложения:
# app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import asyncio
import json
import time
from typing import Optional
import redis.asyncio as redis
app = FastAPI()
r = redis.from_url("redis://localhost:6379/0", decode_responses=True)
CACHE_TTL = 60 # основной TTL
STALE_TTL = 15 # окно, в которое можно отдавать устаревшие данные
LOCK_TTL = 10 # сколько держать замок на пересборку
class Product(BaseModel):
id: int
name: str
price: float
stock: int
updated_at: float
# Заглушка: якобы дорогой запрос к БД
async def fetch_product_from_db(product_id: int) -> Product:
await asyncio.sleep(0.2) # имитация задержки БД/логики
return Product(
id=product_id,
name=f"Товар #{product_id}",
price=1999.0 + (product_id % 7) * 50,
stock=42,
updated_at=time.time()
)
async def get_with_cache(product_id: int) -> dict:
key = f"product:{product_id}:v1" # включаем версию схемы в ключ
lock_key = f"lock:{key}"
raw = await r.get(key)
if raw:
data = json.loads(raw)
# Проверяем, не устарело ли полностью
if time.time() - data["_cached_at"] < CACHE_TTL:
return {"data": data, "cached": True, "stale": False}
# В пределах STALE_TTL разрешаем отдать старое, пока один поток обновляет
if time.time() - data["_cached_at"] < CACHE_TTL + STALE_TTL:
# Пытаемся поставить замок на обновление
got_lock = await r.set(lock_key, "1", nx=True, ex=LOCK_TTL)
if got_lock:
# Фоновой таск обновления (без ожидания клиента)
asyncio.create_task(refresh_cache(key, product_id, lock_key))
return {"data": data, "cached": True, "stale": True}
# Иначе — кэш слишком старый, падаем на обновление ниже
# Нет кэша или он слишком старый: ставим замок
got_lock = await r.set(lock_key, "1", nx=True, ex=LOCK_TTL)
if got_lock:
# Мы ответственные за обновление
product = await fetch_product_from_db(product_id)
payload = product.model_dump()
payload["_cached_at"] = time.time()
await r.set(key, json.dumps(payload), ex=CACHE_TTL + STALE_TTL)
await r.delete(lock_key)
return {"data": payload, "cached": False, "stale": False}
else:
# Кто-то уже обновляет: коротко ждём и пробуем снова
await asyncio.sleep(0.05)
raw2 = await r.get(key)
if raw2:
data2 = json.loads(raw2)
return {"data": data2, "cached": True, "stale": True}
# Совсем ничего нет — ждём ещё и запрашиваем напрямую, чтобы не завис
product = await fetch_product_from_db(product_id)
payload = product.model_dump()
payload["_cached_at"] = time.time()
await r.set(key, json.dumps(payload), ex=CACHE_TTL + STALE_TTL)
return {"data": payload, "cached": False, "stale": False}
async def refresh_cache(key: str, product_id: int, lock_key: str):
try:
product = await fetch_product_from_db(product_id)
payload = product.model_dump()
payload["_cached_at"] = time.time()
await r.set(key, json.dumps(payload), ex=CACHE_TTL + STALE_TTL)
finally:
await r.delete(lock_key)
@app.get("/products/{product_id}")
async def product_endpoint(product_id: int):
if product_id <= 0:
raise HTTPException(status_code=400, detail="invalid id")
result = await get_with_cache(product_id)
headers = {
"X-Cache": "HIT" if result["cached"] else "MISS",
"X-Cache-Stale": "1" if result["stale"] else "0",
# Разрешим прокси держать ответ короткое время (см. микрокэш ниже)
"Cache-Control": "public, max-age=30, stale-while-revalidate=30"
}
return result["data"], headers
if __name__ == "__main__":
import uvicorn
uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=False)
Запуск:
python app.py
Что даёт пример:
Микрокэш полезен для публичных GET-запросов. Он держит ответ 1–10 секунд, перекрывая пики. Короткий TTL безопасен: пользователь почти не заметит, а сервер разгружается.
Пример конфига:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:50m max_size=1g inactive=60m use_temp_path=off;
map $request_method $no_cache {
default 0;
POST 1;
PUT 1;
PATCH 1;
DELETE 1;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache app_cache;
proxy_cache_bypass $no_cache;
proxy_no_cache $no_cache;
proxy_cache_valid 200 302 10s; # микрокэш 10 секунд для успешных ответов
proxy_cache_valid 404 1s; # коротко кэшируем 404, чтобы не бомбили
add_header X-Proxy-Cache $upstream_cache_status;
}
}
Важно: для страниц, зависящих от авторизации, микрокэш нужно отключать или сегментировать ключи по куке/токену.
Если аудитория географически распределена или у вас много статики и «почти статичных» страниц, CDN уменьшает задержки. Что отдавать через CDN:
Рекомендации:
Cache-Control: public, max-age=60, stale-while-revalidate=60.Vary: Accept-Encoding, Accept-Language.Для персонализированных страниц: используйте Cache-Control: private, no-store.
Проверяем до и после внедрения:
# Установить wrk (Linux)
sudo apt-get install wrk -y
# Нагрузка 30 секунд, 4 потока, 200 одновременных соединений
wrk -t4 -c200 -d30s http://localhost:8000/products/123
Что смотреть:
Серверные метрики: утилизация CPU бэкенда и БД, количество активных соединений, время отклика БД.
Предположим:
Итог:
Неделя 1:
Неделя 2:
Резюме: правильно настроенный кэш — это быстрые страницы, более довольные пользователи и меньшие счета за инфраструктуру. Начиная с горячих точек и коротких TTL, вы в течение 1–2 недель можете добиться ощутимого прироста скорости без больших переработок кода.