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