Files
aiform_dev/backend/app/services/redis_service.py

147 lines
5.1 KiB
Python
Raw Normal View History

"""
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)
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()