Files
Aether/src/clients/redis_client.py

350 lines
11 KiB
Python
Raw Normal View History

2025-12-10 20:52:44 +08:00
"""
全局Redis客户端管理
提供统一的Redis客户端访问确保所有服务使用同一个连接池
熔断器说明:
- 连续失败达到阈值后开启熔断
- 熔断期间返回明确的状态而非静默失败
- 调用方可以根据状态决定降级策略
"""
import os
import time
from enum import Enum
from typing import Optional
import redis.asyncio as aioredis
from src.core.logger import logger
from redis.asyncio import sentinel as redis_sentinel
class RedisState(Enum):
"""Redis 连接状态"""
NOT_INITIALIZED = "not_initialized" # 未初始化
CONNECTED = "connected" # 已连接
CIRCUIT_OPEN = "circuit_open" # 熔断中
DISCONNECTED = "disconnected" # 断开连接
class RedisClientManager:
"""
Redis客户端管理器单例
提供 Redis 连接管理熔断器保护和状态监控
"""
_instance: Optional["RedisClientManager"] = None
_redis: Optional[aioredis.Redis] = None
def __new__(cls):
"""单例模式"""
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
# 避免重复初始化
if getattr(self, "_initialized", False):
return
self._initialized = True
self._circuit_open_until: Optional[float] = None
self._consecutive_failures: int = 0
self._circuit_threshold = int(os.getenv("REDIS_CIRCUIT_BREAKER_THRESHOLD", "3"))
self._circuit_reset_seconds = int(os.getenv("REDIS_CIRCUIT_BREAKER_RESET_SECONDS", "60"))
self._last_error: Optional[str] = None # 记录最后一次错误
def get_state(self) -> RedisState:
"""
获取 Redis 连接状态
Returns:
当前连接状态枚举值
"""
if self._redis is not None:
return RedisState.CONNECTED
if self._circuit_open_until and time.time() < self._circuit_open_until:
return RedisState.CIRCUIT_OPEN
if self._last_error:
return RedisState.DISCONNECTED
return RedisState.NOT_INITIALIZED
def get_circuit_info(self) -> dict:
"""
获取熔断器详细信息
Returns:
包含熔断器状态的字典
"""
state = self.get_state()
info = {
"state": state.value,
"consecutive_failures": self._consecutive_failures,
"circuit_threshold": self._circuit_threshold,
"last_error": self._last_error,
}
if state == RedisState.CIRCUIT_OPEN and self._circuit_open_until:
info["circuit_remaining_seconds"] = max(0, self._circuit_open_until - time.time())
return info
def reset_circuit_breaker(self) -> None:
"""
手动重置熔断器用于管理后台紧急恢复
"""
logger.info("Redis 熔断器手动重置")
self._circuit_open_until = None
self._consecutive_failures = 0
self._last_error = None
async def initialize(self, require_redis: bool = False) -> Optional[aioredis.Redis]:
"""
初始化Redis连接
Args:
require_redis: 是否强制要求Redis连接成功如果为True则连接失败时抛出异常
Returns:
Redis客户端实例如果连接失败返回None当require_redis=False时
Raises:
RuntimeError: 当require_redis=True且连接失败时
"""
if self._redis is not None:
return self._redis
# 检查熔断状态
if self._circuit_open_until and time.time() < self._circuit_open_until:
remaining = self._circuit_open_until - time.time()
logger.warning(
"Redis 客户端处于熔断状态,跳过初始化,剩余 %.1f 秒 (last_error: %s)",
remaining,
self._last_error,
)
if require_redis:
raise RuntimeError(
f"Redis 处于熔断状态,剩余 {remaining:.1f} 秒。"
f"最后错误: {self._last_error}"
"使用管理 API 重置熔断器或等待自动恢复。"
)
return None
# 优先使用 REDIS_URL如果没有则根据密码构建 URL
redis_url = os.getenv("REDIS_URL")
redis_max_conn = int(os.getenv("REDIS_MAX_CONNECTIONS", "50"))
sentinel_hosts = os.getenv("REDIS_SENTINEL_HOSTS")
sentinel_service = os.getenv("REDIS_SENTINEL_SERVICE_NAME", "mymaster")
redis_password = os.getenv("REDIS_PASSWORD")
if not redis_url and not sentinel_hosts:
# 本地开发模式:从 REDIS_PASSWORD 构建 URL
if redis_password:
redis_url = f"redis://:{redis_password}@localhost:6379/0"
else:
redis_url = "redis://localhost:6379/0"
try:
if sentinel_hosts:
sentinel_list = []
for host in sentinel_hosts.split(","):
host = host.strip()
if not host:
continue
if ":" in host:
hostname, port = host.split(":", 1)
sentinel_list.append((hostname, int(port)))
else:
sentinel_list.append((host, 26379))
sentinel_kwargs = {
"password": redis_password,
"socket_timeout": 5.0,
}
sentinel = redis_sentinel.Sentinel(
sentinel_list,
**sentinel_kwargs,
)
self._redis = sentinel.master_for(
service_name=sentinel_service,
max_connections=redis_max_conn,
decode_responses=True,
socket_connect_timeout=5.0,
)
safe_url = f"sentinel://{sentinel_service}"
else:
self._redis = await aioredis.from_url(
redis_url,
encoding="utf-8",
decode_responses=True,
socket_timeout=5.0,
socket_connect_timeout=5.0,
max_connections=redis_max_conn,
)
safe_url = redis_url.split("@")[-1] if "@" in redis_url else redis_url
# 测试连接
await self._redis.ping()
logger.info(f"[OK] 全局Redis客户端初始化成功: {safe_url}")
self._consecutive_failures = 0
self._circuit_open_until = None
return self._redis
except Exception as e:
error_msg = str(e)
self._last_error = error_msg
logger.error(f"[ERROR] Redis连接失败: {error_msg}")
self._consecutive_failures += 1
if self._consecutive_failures >= self._circuit_threshold:
self._circuit_open_until = time.time() + self._circuit_reset_seconds
logger.warning(
"Redis 初始化连续失败 %s 次,开启熔断 %s 秒。"
"熔断期间以下功能将降级: 缓存亲和性、分布式并发控制、RPM限流。"
"可通过管理 API /api/admin/system/redis/reset-circuit 手动重置。",
self._consecutive_failures,
self._circuit_reset_seconds,
)
if require_redis:
# 强制要求Redis时抛出异常拒绝启动
raise RuntimeError(
f"Redis连接失败: {error_msg}\n"
"缓存亲和性功能需要Redis支持请确保Redis服务正常运行。\n"
"检查事项:\n"
"1. Redis服务是否已启动docker-compose up -d redis\n"
"2. 环境变量 REDIS_URL 或 REDIS_PASSWORD 是否配置正确\n"
"3. Redis端口默认6379是否可访问"
) from e
logger.warning(
"[WARN] Redis 不可用,以下功能将降级运行(仅在单实例环境下安全):\n"
" - 缓存亲和性: 禁用(每次请求随机选择 Endpoint\n"
" - 分布式并发控制: 降级为本地计数\n"
" - RPM 限流: 降级为本地限流"
)
self._redis = None
return None
async def close(self) -> None:
"""关闭Redis连接"""
if self._redis:
await self._redis.close()
self._redis = None
logger.info("全局Redis客户端已关闭")
def get_client(self) -> Optional[aioredis.Redis]:
"""
获取Redis客户端非异步
注意必须先调用initialize()初始化
Returns:
Redis客户端实例或None
"""
return self._redis
# 全局单例
_redis_manager: Optional[RedisClientManager] = None
async def get_redis_client(require_redis: bool = False) -> Optional[aioredis.Redis]:
"""
获取全局Redis客户端
Args:
require_redis: 是否强制要求Redis连接成功如果为True则连接失败时抛出异常
Returns:
Redis客户端实例如果未初始化或连接失败返回None当require_redis=False时
Raises:
RuntimeError: 当require_redis=True且连接失败时
"""
global _redis_manager
if _redis_manager is None:
_redis_manager = RedisClientManager()
# 如果尚未连接(例如启动时降级、或 close() 后),尝试重新初始化。
# initialize() 内部包含熔断器逻辑,避免频繁重试导致抖动。
if _redis_manager.get_client() is None:
2025-12-10 20:52:44 +08:00
await _redis_manager.initialize(require_redis=require_redis)
return _redis_manager.get_client()
def get_redis_client_sync() -> Optional[aioredis.Redis]:
"""
同步获取Redis客户端不会初始化
Returns:
Redis客户端实例或None
"""
global _redis_manager
if _redis_manager is None:
return None
return _redis_manager.get_client()
async def close_redis_client() -> None:
"""关闭全局Redis客户端"""
global _redis_manager
if _redis_manager:
await _redis_manager.close()
def get_redis_state() -> RedisState:
"""
获取 Redis 连接状态同步方法
Returns:
Redis 连接状态枚举
"""
global _redis_manager
if _redis_manager is None:
return RedisState.NOT_INITIALIZED
return _redis_manager.get_state()
def get_redis_circuit_info() -> dict:
"""
获取 Redis 熔断器详细信息同步方法
Returns:
熔断器状态字典
"""
global _redis_manager
if _redis_manager is None:
return {
"state": RedisState.NOT_INITIALIZED.value,
"consecutive_failures": 0,
"circuit_threshold": 3,
"last_error": None,
}
return _redis_manager.get_circuit_info()
def reset_redis_circuit_breaker() -> bool:
"""
手动重置 Redis 熔断器同步方法
Returns:
是否成功重置
"""
global _redis_manager
if _redis_manager is None:
return False
_redis_manager.reset_circuit_breaker()
return True