24 KiB
Система кеширования 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. Основные функции кеширования
Структура ключей
Вместо генерации ключей через вспомогательные функции, система следует строгим конвенциям формирования ключей:
-
Ключи для отдельных сущностей строятся по шаблону:
entity:property:value
Например:
topic:id:123
- тема с ID 123author:slug:john-doe
- автор со слагом "john-doe"shout:id:456
- публикация с ID 456
-
Ключи для коллекций строятся по шаблону:
entity:collection[:filter1=value1:filter2=value2:...]
Например:
topics:all:basic
- базовый список всех темauthors:stats:limit=10:offset=0:sort=name
- отсортированный список авторов с пагинациейshouts:feed:limit=20:community=1
- лента публикаций с фильтром по сообществу
-
Специальные форматы ключей для обратной совместимости:
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 |
Важные замечания по использованию ключей
- При инвалидации кеша публикаций через
invalidate_shouts_cache()
необходимо передавать список ID публикаций, а не ключи кеша. - Функция
invalidate_shout_related_cache()
автоматически инвалидирует все связанные ключи для публикации, включая ключи авторов и тем. - Для большинства операций с кешем следует использовать асинхронные функции с префиксом
await
. - При создании новых ключей кеша следует придерживаться существующих конвенций именования.
Отладка и мониторинг
Система кеширования использует логгер для отслеживания операций:
logger.debug(f"Данные получены из кеша по ключу {key}")
logger.debug(f"Удалено {len(keys)} ключей кеша с префиксом {prefix}")
logger.error(f"Ошибка при инвалидации кеша: {e}")
Это позволяет отслеживать работу кеша и выявлять возможные проблемы на ранних стадиях.
Рекомендации по использованию
- Следуйте конвенциям формирования ключей - это критически важно для консистентности и предсказуемости кеша.
- Не создавайте собственные форматы ключей - используйте существующие шаблоны для обеспечения единообразия.
- Не забывайте об инвалидации - всегда инвалидируйте кеш при изменении данных.
- Используйте точечную инвалидацию - вместо инвалидации по префиксу для снижения нагрузки на Redis.
- Устанавливайте разумные TTL - используйте разные значения TTL в зависимости от частоты изменения данных.
- Не кешируйте большие объемы данных - кешируйте только то, что действительно необходимо для повышения производительности.
Технические детали реализации
- Сериализация данных: используется
orjson
для эффективной сериализации и десериализации данных. - Форматирование даты и времени: для корректной работы с датами используется
CustomJSONEncoder
. - Асинхронность: все операции кеширования выполняются асинхронно для минимального влияния на производительность API.
- Прямое взаимодействие с Redis: все операции выполняются через прямые вызовы
redis.execute()
с обработкой ошибок. - Батчевая обработка: для массовых операций используется пороговое значение, после которого применяются оптимизированные стратегии.
Известные ограничения
- Согласованность данных - система не гарантирует абсолютную согласованность данных в кеше и базе данных.
- Память - необходимо следить за объемом данных в кеше, чтобы избежать проблем с памятью Redis.
- Производительность Redis - при большом количестве операций с кешем может стать узким местом.