# Система кеширования 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`: ```python 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 Во всех модулях системы разработчики должны явно формировать ключи в соответствии с этими конвенциями, что обеспечивает единообразие и предсказуемость кеширования. #### Работа с данными в кеше ```python async def cache_data(key, data, ttl=None) async def get_cached_data(key) ``` Эти функции предоставляют универсальный интерфейс для сохранения и получения данных из кеша. Они напрямую используют Redis через вызовы `redis.execute()`. #### Высокоуровневое кеширование запросов ```python async def cached_query(cache_key, query_func, ttl=None, force_refresh=False, **query_params) ``` Функция `cached_query` объединяет получение данных из кеша и выполнение запроса в случае отсутствия данных в кеше. Это основная функция, которую следует использовать в резолверах для кеширования результатов запросов. ### 3. Кеширование сущностей Для основных типов сущностей реализованы специальные функции: ```python 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. Работа со связями Для работы со связями между сущностями предназначены функции: ```python 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. Инвалидация по префиксу ```python async def invalidate_cache_by_prefix(prefix) ``` Позволяет инвалидировать все ключи кеша, начинающиеся с указанного префикса. Используется в резолверах для инвалидации группы кешей при массовых изменениях. #### 1.2. Точечная инвалидация ```python async def invalidate_authors_cache(author_id=None) async def invalidate_topics_cache(topic_id=None) ``` Эти функции позволяют инвалидировать кеш только для конкретной сущности, что снижает нагрузку на Redis и предотвращает ненужную потерю кешированных данных. Если ID сущности не указан, используется инвалидация по префиксу. Примеры использования точечной инвалидации: ```python # Инвалидация кеша только для автора с ID 123 await invalidate_authors_cache(123) # Инвалидация кеша только для темы с ID 456 await invalidate_topics_cache(456) ``` ### 2. Отложенная инвалидация Модуль `revalidator.py` реализует систему отложенной инвалидации кеша через класс `CacheRevalidationManager`: ```python 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, которые автоматически отмечают сущности для ревалидации при изменении данных в базе: ```python def events_register(): event.listen(Author, "after_update", mark_for_revalidation) event.listen(Topic, "after_update", mark_for_revalidation) # и другие... ``` Триггеры имеют следующие особенности: - Реагируют на события вставки, обновления и удаления - Отмечают затронутые сущности для отложенной ревалидации - Учитывают связи между сущностями (например, при изменении темы обновляются связанные шауты) ## Предварительное кеширование Модуль `precache.py` реализует предварительное кеширование часто используемых данных при старте приложения: ```python async def precache_data(): # ... ``` Эта функция выполняется при запуске приложения и заполняет кеш данными, которые будут часто запрашиваться пользователями. ## Примеры использования ### Простое кеширование результата запроса ```python 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 ```python 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 ) ``` ### Точечная инвалидация кеша при изменении данных ```python async def update_topic(topic_id, new_data): # Обновление данных в базе # ... # Точечная инвалидация кеша только для измененной темы await invalidate_topics_cache(topic_id) return updated_topic ``` ## Отладка и мониторинг Система кеширования использует логгер для отслеживания операций: ```python 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** - при большом количестве операций с кешем может стать узким местом.