280 lines
15 KiB
Markdown
280 lines
15 KiB
Markdown
|
# Система кеширования 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:
|
|||
|
# ...
|
|||
|
async def process_revalidation(self):
|
|||
|
# ...
|
|||
|
def mark_for_revalidation(self, entity_id, entity_type):
|
|||
|
# ...
|
|||
|
```
|
|||
|
|
|||
|
Менеджер ревалидации работает как асинхронный фоновый процесс, который периодически (по умолчанию каждые 5 минут) проверяет наличие сущностей для ревалидации.
|
|||
|
|
|||
|
Особенности реализации:
|
|||
|
- Для авторов и тем используется поштучная ревалидация каждой записи
|
|||
|
- Для шаутов и реакций используется батчевая обработка, с порогом в 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** - при большом количестве операций с кешем может стать узким местом.
|