core/docs/caching.md

24 KiB
Raw Permalink Blame History

Система кеширования Discours

Общее описание

Система кеширования Discours - это комплексное решение для повышения производительности платформы. Она использует Redis для хранения часто запрашиваемых данных и уменьшения нагрузки на основную базу данных.

Кеширование реализовано как многоуровневая система, состоящая из нескольких модулей:

  • cache.py - основной модуль с функциями кеширования
  • revalidator.py - асинхронный менеджер ревалидации кеша
  • triggers.py - триггеры событий SQLAlchemy для автоматической ревалидации
  • precache.py - предварительное кеширование данных при старте приложения

Ключевые компоненты

1. Форматы ключей кеша

Система поддерживает несколько форматов ключей для обеспечения совместимости и удобства использования:

  • Ключи сущностей: entity:property:value (например, author:id:123)
  • Ключи коллекций: entity:collection:params (например, authors:stats:limit=10:offset=0)
  • Специальные ключи: для обратной совместимости (например, topic_shouts_123)

Все стандартные форматы ключей хранятся в словаре CACHE_KEYS:

CACHE_KEYS = {
    "TOPIC_ID": "topic:id:{}",
    "TOPIC_SLUG": "topic:slug:{}",
    "AUTHOR_ID": "author:id:{}",
    # и другие...
}

2. Основные функции кеширования

Структура ключей

Вместо генерации ключей через вспомогательные функции, система следует строгим конвенциям формирования ключей:

  1. Ключи для отдельных сущностей строятся по шаблону:

    entity:property:value
    

    Например:

    • topic:id:123 - тема с ID 123
    • author:slug:john-doe - автор со слагом "john-doe"
    • shout:id:456 - публикация с ID 456
  2. Ключи для коллекций строятся по шаблону:

    entity:collection[:filter1=value1:filter2=value2:...]
    

    Например:

    • topics:all:basic - базовый список всех тем
    • authors:stats:limit=10:offset=0:sort=name - отсортированный список авторов с пагинацией
    • shouts:feed:limit=20:community=1 - лента публикаций с фильтром по сообществу
  3. Специальные форматы ключей для обратной совместимости:

    entity_action_id
    

    Например:

    • topic_shouts_123 - публикации для темы с ID 123

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

Работа с данными в кеше

async def cache_data(key, data, ttl=None)
async def get_cached_data(key)

Эти функции предоставляют универсальный интерфейс для сохранения и получения данных из кеша. Они напрямую используют Redis через вызовы redis.execute().

Высокоуровневое кеширование запросов

async def cached_query(cache_key, query_func, ttl=None, force_refresh=False, **query_params)

Функция cached_query объединяет получение данных из кеша и выполнение запроса в случае отсутствия данных в кеше. Это основная функция, которую следует использовать в резолверах для кеширования результатов запросов.

3. Кеширование сущностей

Для основных типов сущностей реализованы специальные функции:

async def cache_topic(topic: dict)
async def cache_author(author: dict)
async def get_cached_topic(topic_id: int)
async def get_cached_author(author_id: int, get_with_stat)

Эти функции упрощают работу с часто используемыми типами данных и обеспечивают единообразный подход к их кешированию.

4. Работа со связями

Для работы со связями между сущностями предназначены функции:

async def cache_follows(follower_id, entity_type, entity_id, is_insert=True)
async def get_cached_topic_followers(topic_id)
async def get_cached_author_followers(author_id)
async def get_cached_follower_topics(author_id)

Они позволяют эффективно кешировать и получать информацию о подписках, связях между авторами, темами и публикациями.

Система инвалидации кеша

1. Прямая инвалидация

Система поддерживает два типа инвалидации кеша:

1.1. Инвалидация по префиксу

async def invalidate_cache_by_prefix(prefix)

Позволяет инвалидировать все ключи кеша, начинающиеся с указанного префикса. Используется в резолверах для инвалидации группы кешей при массовых изменениях.

1.2. Точечная инвалидация

async def invalidate_authors_cache(author_id=None)
async def invalidate_topics_cache(topic_id=None)

Эти функции позволяют инвалидировать кеш только для конкретной сущности, что снижает нагрузку на Redis и предотвращает ненужную потерю кешированных данных. Если ID сущности не указан, используется инвалидация по префиксу.

Примеры использования точечной инвалидации:

# Инвалидация кеша только для автора с ID 123
await invalidate_authors_cache(123)

# Инвалидация кеша только для темы с ID 456
await invalidate_topics_cache(456)

2. Отложенная инвалидация

Модуль revalidator.py реализует систему отложенной инвалидации кеша через класс CacheRevalidationManager:

class CacheRevalidationManager:
    def __init__(self, interval=CACHE_REVALIDATION_INTERVAL):
        # ...
        self._redis = redis  # Прямая ссылка на сервис Redis

    async def start(self):
        # Проверка и установка соединения с Redis
        # ...

    async def process_revalidation(self):
        # Обработка элементов для ревалидации
        # ...

    def mark_for_revalidation(self, entity_id, entity_type):
        # Добавляет сущность в очередь на ревалидацию
        # ...

Менеджер ревалидации работает как асинхронный фоновый процесс, который периодически (по умолчанию каждые 5 минут) проверяет наличие сущностей для ревалидации.

Взаимодействие с Redis:

  • CacheRevalidationManager хранит прямую ссылку на сервис Redis через атрибут _redis
  • При запуске проверяется наличие соединения с Redis и при необходимости устанавливается новое
  • Включена автоматическая проверка соединения перед каждой операцией ревалидации
  • Система самостоятельно восстанавливает соединение при его потере

Особенности реализации:

  • Для авторов и тем используется поштучная ревалидация каждой записи
  • Для шаутов и реакций используется батчевая обработка, с порогом в 10 элементов
  • При достижении порога система переключается на инвалидацию коллекций вместо поштучной обработки
  • Специальный флаг all позволяет запустить полную инвалидацию всех записей типа

3. Автоматическая инвалидация через триггеры

Модуль triggers.py регистрирует обработчики событий SQLAlchemy, которые автоматически отмечают сущности для ревалидации при изменении данных в базе:

def events_register():
    event.listen(Author, "after_update", mark_for_revalidation)
    event.listen(Topic, "after_update", mark_for_revalidation)
    # и другие...

Триггеры имеют следующие особенности:

  • Реагируют на события вставки, обновления и удаления
  • Отмечают затронутые сущности для отложенной ревалидации
  • Учитывают связи между сущностями (например, при изменении темы обновляются связанные шауты)

Предварительное кеширование

Модуль precache.py реализует предварительное кеширование часто используемых данных при старте приложения:

async def precache_data():
    # ...

Эта функция выполняется при запуске приложения и заполняет кеш данными, которые будут часто запрашиваться пользователями.

Примеры использования

Простое кеширование результата запроса

async def get_topics_with_stats(limit=10, offset=0, by="title"):
    # Формирование ключа кеша по конвенции
    cache_key = f"topics:stats:limit={limit}:offset={offset}:sort={by}"

    cached_data = await get_cached_data(cache_key)
    if cached_data:
        return cached_data

    # Выполнение запроса к базе данных
    result = ... # логика получения данных

    await cache_data(cache_key, result, ttl=300)
    return result

Использование обобщенной функции cached_query

async def get_topics_with_stats(limit=10, offset=0, by="title"):
    async def fetch_data(limit, offset, by):
        # Логика получения данных
        return result

    # Формирование ключа кеша по конвенции
    cache_key = f"topics:stats:limit={limit}:offset={offset}:sort={by}"

    return await cached_query(
        cache_key,
        fetch_data,
        ttl=300,
        limit=limit,
        offset=offset,
        by=by
    )

Точечная инвалидация кеша при изменении данных

async def update_author(author_id, data):
    # Обновление данных в базе
    # ...

    # Инвалидация только кеша этого автора
    await invalidate_authors_cache(author_id)

    return result

Ключи кеширования

Ниже приведен полный список форматов ключей, используемых в системе кеширования Discours.

Ключи для публикаций (Shout)

Формат ключа Описание Пример
shouts:{id} Публикация по ID shouts:123
shouts:{id}:invalidated Флаг инвалидации публикации shouts:123:invalidated
shouts:feed:limit={n}:offset={m} Основная лента публикаций shouts:feed:limit=20:offset=0
shouts:recent:limit={n} Последние публикации shouts:recent:limit=10
shouts:random_top:limit={n} Случайные топовые публикации shouts:random_top:limit=5
shouts:unrated:limit={n} Неоцененные публикации shouts:unrated:limit=20
shouts:coauthored:limit={n} Совместные публикации shouts:coauthored:limit=10

Ключи для авторов (Author)

Формат ключа Описание Пример
author:id:{id} Автор по ID author:id:123
author:slug:{slug} Автор по слагу author:slug:john-doe
author:user_id:{user_id} Автор по ID пользователя author:user_id:abc123
author:{id} Публикации автора author:123
authored:{id} Публикации, созданные автором authored:123
authors:all:basic Базовый список всех авторов authors:all:basic
authors:stats:limit={n}:offset={m}:sort={field} Список авторов с пагинацией и сортировкой authors:stats:limit=20:offset=0:sort=name
author:followers:{id} Подписчики автора author:followers:123
author:following:{id} Авторы, на которых подписан автор author:following:123

Ключи для тем (Topic)

Формат ключа Описание Пример
topic:id:{id} Тема по ID topic:id:123
topic:slug:{slug} Тема по слагу topic:slug:technology
topic:{id} Публикации по теме topic:123
topic_shouts_{id} Публикации по теме (старый формат) topic_shouts_123
topics:all:basic Базовый список всех тем topics:all:basic
topics:stats:limit={n}:offset={m}:sort={field} Список тем с пагинацией и сортировкой topics:stats:limit=20:offset=0:sort=name
topic:authors:{id} Авторы темы topic:authors:123
topic:followers:{id} Подписчики темы topic:followers:123
topic:stats:{id} Статистика темы topic:stats:123

Ключи для реакций (Reaction)

Формат ключа Описание Пример
reactions:shout:{id}:limit={n}:offset={m} Реакции на публикацию reactions:shout:123:limit=20:offset=0
reactions:comment:{id}:limit={n}:offset={m} Реакции на комментарий reactions:comment:456:limit=20:offset=0
reactions:author:{id}:limit={n}:offset={m} Реакции автора reactions:author:123:limit=20:offset=0
reactions:followed:author:{id}:limit={n} Реакции авторов, на которых подписан пользователь reactions:followed:author:123:limit=20

Ключи для сообществ (Community)

Формат ключа Описание Пример
community:id:{id} Сообщество по ID community:id:123
community:slug:{slug} Сообщество по слагу community:slug:tech-club
communities:all:basic Базовый список всех сообществ communities:all:basic
community:authors:{id} Авторы сообщества community:authors:123
community:shouts:{id}:limit={n}:offset={m} Публикации сообщества community:shouts:123:limit=20:offset=0

Ключи для подписок (Follow)

Формат ключа Описание Пример
follow:author:{follower_id}:authors Авторы, на которых подписан пользователь follow:author:123:authors
follow:author:{follower_id}:topics Темы, на которые подписан пользователь follow:author:123:topics
follow:topic:{topic_id}:authors Авторы, подписанные на тему follow:topic:456:authors
follow:author:{author_id}:followers Подписчики автора follow:author:123:followers

Ключи для черновиков (Draft)

Формат ключа Описание Пример
draft:id:{id} Черновик по ID draft:id:123
drafts:author:{id} Черновики автора drafts:author:123
drafts:all:limit={n}:offset={m} Список всех черновиков с пагинацией drafts:all:limit=20:offset=0

Ключи для статистики

Формат ключа Описание Пример
stats:shout:{id} Статистика публикации stats:shout:123
stats:author:{id} Статистика автора stats:author:123
stats:topic:{id} Статистика темы stats:topic:123
stats:community:{id} Статистика сообщества stats:community:123

Ключи для поиска

Формат ключа Описание Пример
search:query:{query}:limit={n}:offset={m} Результаты поиска search:query:технологии:limit=20:offset=0
search:author:{query}:limit={n} Результаты поиска авторов search:author:иван:limit=10
search:topic:{query}:limit={n} Результаты поиска тем search:topic:наука:limit=10

Служебные ключи

Формат ключа Описание Пример
revalidation:{entity_type}:{entity_id} Метка для ревалидации revalidation:author:123
revalidation:batch:{entity_type} Батчевая ревалидация revalidation:batch:shouts
lock:{resource} Блокировка ресурса lock:precache
views:shout:{id} Счетчик просмотров публикации views:shout:123

Важные замечания по использованию ключей

  1. При инвалидации кеша публикаций через invalidate_shouts_cache() необходимо передавать список ID публикаций, а не ключи кеша.
  2. Функция invalidate_shout_related_cache() автоматически инвалидирует все связанные ключи для публикации, включая ключи авторов и тем.
  3. Для большинства операций с кешем следует использовать асинхронные функции с префиксом await.
  4. При создании новых ключей кеша следует придерживаться существующих конвенций именования.

Отладка и мониторинг

Система кеширования использует логгер для отслеживания операций:

logger.debug(f"Данные получены из кеша по ключу {key}")
logger.debug(f"Удалено {len(keys)} ключей кеша с префиксом {prefix}")
logger.error(f"Ошибка при инвалидации кеша: {e}")

Это позволяет отслеживать работу кеша и выявлять возможные проблемы на ранних стадиях.

Рекомендации по использованию

  1. Следуйте конвенциям формирования ключей - это критически важно для консистентности и предсказуемости кеша.
  2. Не создавайте собственные форматы ключей - используйте существующие шаблоны для обеспечения единообразия.
  3. Не забывайте об инвалидации - всегда инвалидируйте кеш при изменении данных.
  4. Используйте точечную инвалидацию - вместо инвалидации по префиксу для снижения нагрузки на Redis.
  5. Устанавливайте разумные TTL - используйте разные значения TTL в зависимости от частоты изменения данных.
  6. Не кешируйте большие объемы данных - кешируйте только то, что действительно необходимо для повышения производительности.

Технические детали реализации

  • Сериализация данных: используется orjson для эффективной сериализации и десериализации данных.
  • Форматирование даты и времени: для корректной работы с датами используется CustomJSONEncoder.
  • Асинхронность: все операции кеширования выполняются асинхронно для минимального влияния на производительность API.
  • Прямое взаимодействие с Redis: все операции выполняются через прямые вызовы redis.execute() с обработкой ошибок.
  • Батчевая обработка: для массовых операций используется пороговое значение, после которого применяются оптимизированные стратегии.

Известные ограничения

  1. Согласованность данных - система не гарантирует абсолютную согласованность данных в кеше и базе данных.
  2. Память - необходимо следить за объемом данных в кеше, чтобы избежать проблем с памятью Redis.
  3. Производительность Redis - при большом количестве операций с кешем может стать узким местом.