Уяви собі гіперактивний API, який шле запити на сервер кожні півсекунди. Якщо кожен такий запит має звертатись до бази даних за даними, сервер рано чи пізно почне панікувати, а твій DevOps почне пити забагато кави. Ось тут і виручає кешування! Замість того, щоб щоразу лізти в базу, сервіс може тимчасово зберігати результати частих запитів у Redis.
Кешування підходить для даних, які:
- часто запитуються (наприклад, популярні товари в інтернет-магазині),
- рідко змінюються (наприклад, курси валют, які оновлюються раз на добу),
- займають небагато місця (Redis зберігає дані в оперативній пам'яті, і її обсяг обмежений).
Але уникай кешування динамічно змінних даних, бо це може призвести до застарілих або некоректних результатів.
Визначення TTL
TTL (Time to Live) — це час, протягом якого дані залишаються актуальними в кеші. Після його закінчення дані видаляються. Для кожного типу даних треба підбирати свій TTL:
- Швидко оновлювані дані: 30–60 секунд.
- Рідко змінні дані: 5–10 хвилин.
- Майже статичні дані (наприклад, географічна інформація): години або навіть дні.
Налаштування кешування для запиту
Припустимо, у нас є база даних PostgreSQL, у якій зберігається список користувачів. Завдання: створити ендпоінт, який повертає всіх користувачів, і кешувати цей запит у Redis.
Спочатку реалізуємо простий ендпоінт без кешування:
# app.py
from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from models import User # Це модель SQLAlchemy
# Налаштування FastAPI і бази даних
app = FastAPI()
DATABASE_URL = "postgresql+asyncpg://user:password@localhost/mydatabase"
engine = create_async_engine(DATABASE_URL, echo=True)
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
@app.get("/users")
async def get_users():
async with async_session() as session:
# Запит усіх користувачів з бази даних
result = await session.execute("SELECT * FROM users")
users = result.fetchall()
return {"users": [dict(row) for row in users]}
Цей ендпоінт працює, але щоразу звертається до бази даних.
Тепер додамо Redis для кешування. Для цього встановимо бібліотеку redis-py:
pip install redis
Налаштуємо підключення до Redis і реалізуємо кешування:
import redis
import json
# Підключення до Redis
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0, decode_responses=True)
@app.get("/users")
async def get_users():
# Спробуємо знайти дані в кеші
cached_users = redis_client.get("users")
if cached_users:
# Якщо дані знайдені, повернемо їх
return {"users": json.loads(cached_users)}
# Якщо даних у кеші немає, робимо запит до бази даних
async with async_session() as session:
result = await session.execute("SELECT * FROM users")
users = result.fetchall()
# Зберігаємо результат у кеші на 60 секунд
redis_client.setex("users", 60, json.dumps([dict(row) for row in users]))
return {"users": [dict(row) for row in users]}
У цьому коді:
- Спочатку перевіряємо, чи є ключ
"users"у Redis. - Якщо ключ знайдено, беремо дані з кешу.
- Якщо ні, запитуємо дані з бази даних і зберігаємо їх у Redis.
Тепер запит до /users буде працювати набагато швидше!
Оптимізація кешування даних
Для кешування більш складних запитів (наприклад, з параметрами) треба динамічно генерувати унікальний ключ.
@app.get("/users/{user_id}")
async def get_user(user_id: int):
key = f"user:{user_id}" # Генеруємо унікальний ключ
cached_user = redis_client.get(key)
if cached_user:
return {"user": json.loads(cached_user)}
async with async_session() as session:
result = await session.execute(f"SELECT * FROM users WHERE id = {user_id}")
user = result.fetchone()
if user:
redis_client.setex(key, 60, json.dumps(dict(user)))
return {"user": dict(user) if user else None}
Тут ключ кешу залежить від user_id. Це гарантує, що кеш зберігає дані для конкретного користувача.
Інвалідація кешу
Коли дані в базі оновлюються, відповідні дані в кеші стають застарілими. Щоб цього уникнути, потрібно скидати (інвалідувати) кеш.
Припустимо, у нас є ендпоінт для оновлення користувача. Після оновлення потрібно видалити старий кеш:
@app.put("/users/{user_id}")
async def update_user(user_id: int, user_data: dict):
async with async_session() as session:
# Приклад оновлення даних у базі
await session.execute(
f"UPDATE users SET name = :name WHERE id = {user_id}",
{"name": user_data["name"]},
)
await session.commit()
# Видаляємо застарілий кеш
redis_client.delete(f"user:{user_id}")
return {"message": "User updated successfully"}
Тепер, після оновлення користувача, дані в кеші видаляються, і при наступному запиті вони оновляться.
Приклад завершеного кешування для списку
Робота з Redis стає ще гнучкішою з використанням функцій для загального керування кешем:
def get_or_set_cache(key, fetch_func, ttl=60):
# Спробуємо знайти ключ у кеші
cached_data = redis_client.get(key)
if cached_data:
return json.loads(cached_data)
# Якщо такого ключа немає, отримуємо дані і кешуємо
data = fetch_func()
redis_client.setex(key, ttl, json.dumps(data))
return data
@app.get("/users")
async def get_users():
async def fetch_users():
async with async_session() as session:
result = await session.execute("SELECT * FROM users")
return [dict(row) for row in result.fetchall()]
# Використовуємо обгортку для кешування
users = get_or_set_cache("users", fetch_users)
return {"users": users}
Ця реалізація дозволяє повторно використовувати логіку кешування в різних частинах застосунку.
Підсумки
Використання Redis для кешування запитів до бази даних знижує навантаження на сервер і пришвидшує виконання запитів. Ми навчилися:
- Підключати Redis до FastAPI.
- Налаштовувати кеш для запитів із використанням
setexі TTL. - Реалізовувати динамічне кешування з використанням унікальних ключів.
- Оновлювати кеш при зміні даних у базі.
Вітаю, ти щойно зробив(ла) свій застосунок значно швидшим! Однак, щоб не перевантажити твій Redis, не забудь про очищення старих і непотрібних даних. Попереду нас чекає ще більше оптимізацій і секретів кешування!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ