Files
Aether/src/clients/redis_client.py
fawney19 7b932d7afb refactor: optimize middleware with pure ASGI implementation and enhance security measures
- Replace BaseHTTPMiddleware with pure ASGI implementation in plugin middleware for better streaming response handling
- Add trusted proxy count configuration for client IP extraction in reverse proxy environments
- Implement audit log cleanup scheduler with configurable retention period
- Replace plaintext token logging with SHA256 hash fingerprints for security
- Fix database session lifecycle management in middleware
- Improve request tracing and error logging throughout the system
- Add comprehensive tests for pipeline architecture
2025-12-18 19:07:20 +08:00

350 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
全局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: {})",
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 初始化连续失败 {} 次,开启熔断 {} 秒。"
"熔断期间以下功能将降级: 缓存亲和性、分布式并发控制、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:
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