2025-10-24 16:19:58 +03:00
|
|
|
|
"""
|
|
|
|
|
|
Redis Service для кеширования, rate limiting, сессий
|
|
|
|
|
|
"""
|
|
|
|
|
|
import redis.asyncio as redis
|
|
|
|
|
|
from typing import Optional, Any
|
|
|
|
|
|
import json
|
|
|
|
|
|
from ..config import settings
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RedisService:
|
|
|
|
|
|
"""Сервис для работы с Redis"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.client: Optional[redis.Redis] = None
|
|
|
|
|
|
|
|
|
|
|
|
async def connect(self):
|
|
|
|
|
|
"""Подключение к Redis"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.client = await redis.from_url(
|
|
|
|
|
|
settings.redis_url,
|
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
|
decode_responses=True
|
|
|
|
|
|
)
|
|
|
|
|
|
await self.client.ping()
|
|
|
|
|
|
logger.info(f"✅ Redis connected: {settings.redis_host}:{settings.redis_port}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ Redis connection error: {e}")
|
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
|
|
async def disconnect(self):
|
|
|
|
|
|
"""Отключение от Redis"""
|
|
|
|
|
|
if self.client:
|
|
|
|
|
|
await self.client.close()
|
|
|
|
|
|
logger.info("Redis connection closed")
|
|
|
|
|
|
|
|
|
|
|
|
async def get(self, key: str) -> Optional[str]:
|
|
|
|
|
|
"""Получить значение по ключу"""
|
|
|
|
|
|
full_key = f"{settings.redis_prefix}{key}"
|
|
|
|
|
|
return await self.client.get(full_key)
|
|
|
|
|
|
|
|
|
|
|
|
async def set(self, key: str, value: Any, expire: Optional[int] = None):
|
|
|
|
|
|
"""Установить значение с опциональным TTL (в секундах)"""
|
|
|
|
|
|
full_key = f"{settings.redis_prefix}{key}"
|
|
|
|
|
|
if isinstance(value, (dict, list)):
|
|
|
|
|
|
value = json.dumps(value)
|
|
|
|
|
|
if expire:
|
|
|
|
|
|
await self.client.setex(full_key, expire, value)
|
|
|
|
|
|
else:
|
|
|
|
|
|
await self.client.set(full_key, value)
|
|
|
|
|
|
|
2025-10-27 08:33:16 +03:00
|
|
|
|
async def publish(self, channel: str, message: str):
|
|
|
|
|
|
"""Публикация сообщения в канал Redis Pub/Sub"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
await self.client.publish(channel, message)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"❌ Redis publish error: {e}")
|
|
|
|
|
|
|
2025-10-24 16:19:58 +03:00
|
|
|
|
async def delete(self, key: str) -> bool:
|
|
|
|
|
|
"""Удалить ключ"""
|
|
|
|
|
|
full_key = f"{settings.redis_prefix}{key}"
|
|
|
|
|
|
result = await self.client.delete(full_key)
|
|
|
|
|
|
return result > 0
|
|
|
|
|
|
|
|
|
|
|
|
async def exists(self, key: str) -> bool:
|
|
|
|
|
|
"""Проверить существование ключа"""
|
|
|
|
|
|
full_key = f"{settings.redis_prefix}{key}"
|
|
|
|
|
|
return await self.client.exists(full_key) > 0
|
|
|
|
|
|
|
|
|
|
|
|
async def increment(self, key: str, amount: int = 1) -> int:
|
|
|
|
|
|
"""Инкремент значения"""
|
|
|
|
|
|
full_key = f"{settings.redis_prefix}{key}"
|
|
|
|
|
|
return await self.client.incrby(full_key, amount)
|
|
|
|
|
|
|
|
|
|
|
|
async def expire(self, key: str, seconds: int):
|
|
|
|
|
|
"""Установить TTL для ключа"""
|
|
|
|
|
|
full_key = f"{settings.redis_prefix}{key}"
|
|
|
|
|
|
await self.client.expire(full_key, seconds)
|
|
|
|
|
|
|
|
|
|
|
|
async def get_json(self, key: str) -> Optional[dict]:
|
|
|
|
|
|
"""Получить JSON значение"""
|
|
|
|
|
|
value = await self.get(key)
|
|
|
|
|
|
if value:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(value)
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
async def set_json(self, key: str, value: dict, expire: Optional[int] = None):
|
|
|
|
|
|
"""Установить JSON значение"""
|
|
|
|
|
|
await self.set(key, json.dumps(value), expire)
|
|
|
|
|
|
|
|
|
|
|
|
async def health_check(self) -> bool:
|
|
|
|
|
|
"""Проверка здоровья Redis"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
return await self.client.ping()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Redis health check failed: {e}")
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# RATE LIMITING
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
async def check_rate_limit(self, identifier: str, max_requests: int, window_seconds: int) -> tuple[bool, int]:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Проверка rate limiting
|
|
|
|
|
|
Returns: (allowed: bool, remaining: int)
|
|
|
|
|
|
"""
|
|
|
|
|
|
key = f"ratelimit:{identifier}"
|
|
|
|
|
|
full_key = f"{settings.redis_prefix}{key}"
|
|
|
|
|
|
|
|
|
|
|
|
current = await self.client.get(full_key)
|
|
|
|
|
|
|
|
|
|
|
|
if current is None:
|
|
|
|
|
|
# Первый запрос в окне
|
|
|
|
|
|
await self.client.setex(full_key, window_seconds, 1)
|
|
|
|
|
|
return True, max_requests - 1
|
|
|
|
|
|
|
|
|
|
|
|
current_count = int(current)
|
|
|
|
|
|
|
|
|
|
|
|
if current_count >= max_requests:
|
|
|
|
|
|
# Лимит превышен
|
|
|
|
|
|
ttl = await self.client.ttl(full_key)
|
|
|
|
|
|
return False, 0
|
|
|
|
|
|
|
|
|
|
|
|
# Инкремент счетчика
|
|
|
|
|
|
new_count = await self.client.incr(full_key)
|
|
|
|
|
|
return True, max_requests - new_count
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
# CACHE
|
|
|
|
|
|
# ============================================
|
|
|
|
|
|
|
|
|
|
|
|
async def cache_get(self, cache_key: str) -> Optional[Any]:
|
|
|
|
|
|
"""Получить из кеша"""
|
|
|
|
|
|
return await self.get_json(f"cache:{cache_key}")
|
|
|
|
|
|
|
|
|
|
|
|
async def cache_set(self, cache_key: str, value: Any, ttl: int = 3600):
|
|
|
|
|
|
"""Сохранить в кеш (TTL по умолчанию 1 час)"""
|
|
|
|
|
|
await self.set_json(f"cache:{cache_key}", value, ttl)
|
|
|
|
|
|
|
|
|
|
|
|
async def cache_delete(self, cache_key: str):
|
|
|
|
|
|
"""Удалить из кеша"""
|
|
|
|
|
|
await self.delete(f"cache:{cache_key}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Глобальный экземпляр
|
|
|
|
|
|
redis_service = RedisService()
|
|
|
|
|
|
|