16 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_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}")
Это позволяет отслеживать работу кеша и выявлять возможные проблемы на ранних стадиях.
Рекомендации по использованию
- Следуйте конвенциям формирования ключей - это критически важно для консистентности и предсказуемости кеша.
- Не создавайте собственные форматы ключей - используйте существующие шаблоны для обеспечения единообразия.
- Не забывайте об инвалидации - всегда инвалидируйте кеш при изменении данных.
- Используйте точечную инвалидацию - вместо инвалидации по префиксу для снижения нагрузки на Redis.
- Устанавливайте разумные TTL - используйте разные значения TTL в зависимости от частоты изменения данных.
- Не кешируйте большие объемы данных - кешируйте только то, что действительно необходимо для повышения производительности.
Технические детали реализации
- Сериализация данных: используется
orjson
для эффективной сериализации и десериализации данных. - Форматирование даты и времени: для корректной работы с датами используется
CustomJSONEncoder
. - Асинхронность: все операции кеширования выполняются асинхронно для минимального влияния на производительность API.
- Прямое взаимодействие с Redis: все операции выполняются через прямые вызовы
redis.execute()
с обработкой ошибок. - Батчевая обработка: для массовых операций используется пороговое значение, после которого применяются оптимизированные стратегии.
Известные ограничения
- Согласованность данных - система не гарантирует абсолютную согласованность данных в кеше и базе данных.
- Память - необходимо следить за объемом данных в кеше, чтобы избежать проблем с памятью Redis.
- Производительность Redis - при большом количестве операций с кешем может стать узким местом.