JavaRush /Курси /Модуль 4: FastAPI /Налаштування кешу для запитів до бази даних через Redis

Налаштування кешу для запитів до бази даних через Redis

Модуль 4: FastAPI
Рівень 9 , Лекція 3
Відкрита

Уяви собі гіперактивний 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]}

У цьому коді:

  1. Спочатку перевіряємо, чи є ключ "users" у Redis.
  2. Якщо ключ знайдено, беремо дані з кешу.
  3. Якщо ні, запитуємо дані з бази даних і зберігаємо їх у 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, не забудь про очищення старих і непотрібних даних. Попереду нас чекає ще більше оптимізацій і секретів кешування!

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ