core/docs/caching.md
Untone 615f1fe468
All checks were successful
Deploy on push / deploy (push) Successful in 5s
topics+authors-reimplemented-cache
2025-03-22 11:47:19 +03:00

15 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:
    # ...
    async def process_revalidation(self):
        # ...
    def mark_for_revalidation(self, entity_id, entity_type):
        # ...

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

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

  • Для авторов и тем используется поштучная ревалидация каждой записи
  • Для шаутов и реакций используется батчевая обработка, с порогом в 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_topic(topic_id, new_data):
    # Обновление данных в базе
    # ...
    
    # Точечная инвалидация кеша только для измененной темы
    await invalidate_topics_cache(topic_id)
    
    return updated_topic

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

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

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 - при большом количестве операций с кешем может стать узким местом.