Files
Aether/_deprecated_py_src/services/scheduling/affinity_manager.py
fawney19 1d9c77522a refactor: 移除 Python 后端源码,全面迁移至 Rust gateway 架构
- 删除全部 Python 源码 (src/) 及 Alembic 迁移脚本,归档至 _deprecated_py_src/
- 重构 Rust gateway ai_pipeline: 拆分 planner/finalize 模块,新增 contracts/adaptation 层
- 重组 handlers 模块为 admin/public/proxy/internal/shared 子模块结构
- 新增 executor 模块,引入 Rust 原生数据库迁移 (aether-data/migrations)
- 简化 CI/Docker 构建流程,移除 base image 二级构建,统一为单一 app image
- 移除 Python 相关基础设施文件 (entrypoint.sh, gunicorn_conf.py, Dockerfile.base)
2026-04-03 16:26:16 +08:00

758 lines
30 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.
"""
缓存亲和性管理器 (Cache Affinity Manager) - 支持 Redis 或内存存储
职责:
1. 跟踪请求API Key的Provider+Key缓存状态
2. 管理缓存有效期
3. 提供缓存统计和分析
4. 自动失效不支持缓存的Provider
设计原理:
- 每个API Key使用某个Provider的Key后在缓存TTL期内应该继续使用同一个Key
- 这样可以最大化利用提供商的Prompt Caching机制
- 当Key故障时自动失效该Key的缓存亲和性
- 当Provider关闭缓存支持时自动失效所有相关亲和性
注意:
- affinity_key 参数通常为请求使用的 API Key IDapi_key_id
- 这样可以支持"独立余额Key"场景每个Key有自己的缓存亲和性
"""
from __future__ import annotations
import asyncio
import json
import os
import time
from contextlib import asynccontextmanager
from typing import Any, NamedTuple
from src.config.constants import CacheTTL
from src.core.logger import logger
from src.core.redis_utils import delete_redis_keys, scan_delete_pattern
class CacheAffinity(NamedTuple):
"""缓存亲和性信息"""
provider_id: str
endpoint_id: str
key_id: str
api_format: str # API格式 (claude/openai)
model_name: str # 模型名称
created_at: float # 创建时间戳
expire_at: float # 过期时间戳
request_count: int # 使用次数
class CacheAffinityManager:
"""
缓存亲和性管理器(支持 Redis 或内存存储)
存储结构:
----------------------
Key格式: cache_affinity:{affinity_key}:{api_format}:{model_name}
- affinity_key: 通常为请求使用的 API Key ID支持独立余额Key场景
- api_format: API格式 (claude/openai)
- model_name: 模型名称(区分不同模型的缓存亲和性)
Value格式: JSON/Dict
{
"provider_id": "xxx",
"endpoint_id": "yyy",
"key_id": "zzz",
"model_name": "claude-3-5-sonnet-20241022",
"created_at": 1234567890.123,
"expire_at": 1234567890.123,
"request_count": 5
}
TTL: 自动过期
设计改进:
- 每个API Key可以对多个API格式和模型分别维护缓存亲和性
- 不同模型请求使用独立的缓存亲和性,避免模型切换导致的缓存失效
- 某个端点故障切换不会影响其他端点的亲和性
- 更精确的缓存命中率统计
- 支持"独立余额Key"场景每个Key有独立的缓存亲和性
"""
# 默认缓存TTL- 使用统一常量
DEFAULT_CACHE_TTL = CacheTTL.CACHE_AFFINITY
REDIS_SCAN_BATCH_SIZE = 200
REDIS_DELETE_BATCH_SIZE = 500
def __init__(
self, redis_client: Any | None = None, default_ttl: int = DEFAULT_CACHE_TTL
) -> None:
"""
初始化缓存亲和性管理器
Args:
redis_client: Redis客户端可选
default_ttl: 默认缓存TTL
"""
self.redis = redis_client
self.default_ttl = default_ttl
self._memory_store: dict[str, dict[str, Any]] = {}
self._memory_store_max_size: int = 5000 # 内存模式最大条目数
self._memory_lock: asyncio.Lock | None = None
# L1 缓存(即使使用 Redis 也启用,减少网络往返)
# 注意L1 是本地进程内缓存多实例部署时存在短暂不一致窗口TTL 秒级)。
# 当前 TTL 默认 3 秒,对于亲和性路由来说可接受:最坏情况是短暂路由到
# 旧 provider下次请求即可自动修正。如果需要严格一致性将 TTL 设为 0
# 以禁用 L1 缓存,或通过 CacheSyncService 接收 pub/sub 主动失效。
self._l1_cache_ttl = int(os.getenv("CACHE_AFFINITY_L1_TTL", str(CacheTTL.L1_LOCAL)))
self._l1_cache: dict[str, tuple[float, dict[str, Any]]] = {}
self._l1_lock = asyncio.Lock()
self._l1_max_size = int(os.getenv("CACHE_AFFINITY_L1_MAX_SIZE", "1000")) # 最大缓存条目数
self._l1_last_cleanup = time.time()
# 请求级别锁,避免同一用户+端点同时更新造成抖动
self._request_locks: dict[str, asyncio.Lock] = {}
self._request_locks_max_size: int = 500 # 锁字典上限,防止无界增长
# 统计信息
self._stats = {
"total_affinities": 0,
"cache_hits": 0,
"cache_misses": 0,
"cache_invalidations": 0,
"provider_switches": 0,
"key_switches": 0,
}
if self.redis:
logger.debug("CacheAffinityManager: 使用Redis存储")
else:
logger.debug(
"CacheAffinityManager: Redis不可用回退到内存存储(仅适用于单实例/开发环境)"
)
def _is_memory_backend(self) -> bool:
"""是否处于内存模式"""
return self.redis is None
def _get_memory_lock(self) -> asyncio.Lock:
"""懒初始化内存锁"""
if self._memory_lock is None:
self._memory_lock = asyncio.Lock()
return self._memory_lock
def _get_cache_key(self, affinity_key: str, api_format: str, model_name: str) -> str:
"""
生成Redis Key
Args:
affinity_key: 亲和性标识符通常为API Key ID
api_format: API格式 (claude/openai)
model_name: 模型名称
Returns:
格式化的缓存键: cache_affinity:{affinity_key}:{api_format}:{model_name}
"""
return f"cache_affinity:{affinity_key}:{api_format}:{model_name}"
async def _get_l1_entry(self, cache_key: str) -> dict[str, Any] | None:
async with self._l1_lock:
record = self._l1_cache.get(cache_key)
if not record:
return None
expire_at, payload = record
if time.time() > expire_at:
self._l1_cache.pop(cache_key, None)
return None
return dict(payload)
async def _set_l1_entry(self, cache_key: str, payload: dict[str, Any] | None) -> None:
async with self._l1_lock:
if not payload:
self._l1_cache.pop(cache_key, None)
return
expire_at = time.time() + max(1, self._l1_cache_ttl)
self._l1_cache[cache_key] = (expire_at, dict(payload))
# 定期清理过期条目(每 60 秒最多一次)
current_time = time.time()
if current_time - self._l1_last_cleanup > 60:
self._cleanup_l1_cache_unlocked(current_time)
self._l1_last_cleanup = current_time
def _cleanup_l1_cache_unlocked(self, current_time: float) -> int:
"""清理过期的 L1 缓存条目(需要在持有锁的情况下调用)
Returns:
清理的条目数量
"""
expired_keys = [
key for key, (expire_at, _) in self._l1_cache.items() if current_time > expire_at
]
for key in expired_keys:
self._l1_cache.pop(key, None)
# 如果缓存仍然过大,按过期时间排序移除最旧的条目
if len(self._l1_cache) > self._l1_max_size:
sorted_items = sorted(
self._l1_cache.items(), key=lambda x: x[1][0] # 按 expire_at 排序
)
# 移除最旧的 20% 条目
remove_count = len(self._l1_cache) - int(self._l1_max_size * 0.8)
for key, _ in sorted_items[:remove_count]:
self._l1_cache.pop(key, None)
expired_keys.extend([k for k, _ in sorted_items[:remove_count]])
if expired_keys:
logger.debug(
f"L1 缓存清理: 移除 {len(expired_keys)} 个条目,当前 {len(self._l1_cache)}"
)
return len(expired_keys)
@asynccontextmanager
async def _acquire_request_lock(self, cache_key: str) -> None:
lock = self._request_locks.get(cache_key)
if lock is None:
# 超出上限时淘汰未被持有的锁,防止无界增长
if len(self._request_locks) >= self._request_locks_max_size:
free_keys = [k for k, lk in self._request_locks.items() if not lk.locked()]
for k in free_keys[: len(free_keys) // 2 or 1]: # 清理一半空闲锁
del self._request_locks[k]
lock = asyncio.Lock()
self._request_locks[cache_key] = lock
await lock.acquire()
try:
yield
finally:
lock.release()
async def _load_affinity_dict(self, cache_key: str) -> dict[str, Any] | None:
"""读取缓存亲和性字典"""
# 先尝试L1缓存
l1_value = await self._get_l1_entry(cache_key)
if l1_value is not None:
return l1_value
if not self._is_memory_backend():
data = await self.redis.get(cache_key)
if not data:
return None
value = json.loads(data)
await self._set_l1_entry(cache_key, value)
return value
lock = self._get_memory_lock()
async with lock:
record = self._memory_store.get(cache_key)
if record:
await self._set_l1_entry(cache_key, record)
return dict(record) if record else None
async def _save_affinity_dict(
self, cache_key: str, ttl: int, affinity_dict: dict[str, Any]
) -> None:
"""存储缓存亲和性字典"""
if not self._is_memory_backend():
await self.redis.setex(cache_key, ttl, json.dumps(affinity_dict))
await self._set_l1_entry(cache_key, affinity_dict)
return
lock = self._get_memory_lock()
async with lock:
self._memory_store[cache_key] = dict(affinity_dict)
# 超出上限时清理过期条目
if len(self._memory_store) > self._memory_store_max_size:
now = time.time()
expired = [k for k, v in self._memory_store.items() if now > v.get("expire_at", 0)]
for k in expired:
del self._memory_store[k]
await self._set_l1_entry(cache_key, affinity_dict)
async def _delete_affinity_key(self, cache_key: str) -> None:
"""删除缓存亲和性"""
if not self._is_memory_backend():
await self.redis.delete(cache_key)
else:
lock = self._get_memory_lock()
async with lock:
self._memory_store.pop(cache_key, None)
await self._set_l1_entry(cache_key, None)
async def _delete_redis_keys(self, keys: list[str]) -> int:
if self._is_memory_backend() or not keys:
return 0
return await delete_redis_keys(self.redis, keys)
async def _scan_delete_pattern(self, pattern: str) -> int:
if self._is_memory_backend():
return 0
return await scan_delete_pattern(
self.redis,
pattern,
scan_batch_size=self.REDIS_SCAN_BATCH_SIZE,
delete_batch_size=self.REDIS_DELETE_BATCH_SIZE,
)
async def _clear_l1_entries_by_prefix(self, prefix: str) -> int:
"""清理匹配前缀的 L1 本地缓存。"""
async with self._l1_lock:
keys_to_remove = [key for key in self._l1_cache if key.startswith(prefix)]
for key in keys_to_remove:
self._l1_cache.pop(key, None)
return len(keys_to_remove)
async def _snapshot_memory_items(self) -> dict[str, dict[str, Any]]:
"""复制内存存储内容(仅内存模式使用)"""
lock = self._get_memory_lock()
async with lock:
return {k: dict(v) for k, v in self._memory_store.items()}
async def get_affinity(
self, affinity_key: str, api_format: str, model_name: str
) -> CacheAffinity | None:
"""
获取指定亲和性标识符对特定API格式和模型的缓存亲和性
Args:
affinity_key: 亲和性标识符通常为API Key ID
api_format: API格式 (claude/openai)
model_name: 模型名称
Returns:
CacheAffinity对象如果不存在或已过期则返回None
"""
try:
cache_key = self._get_cache_key(affinity_key, api_format, model_name)
async with self._acquire_request_lock(cache_key):
affinity_dict = await self._load_affinity_dict(cache_key)
if not affinity_dict:
self._stats["cache_misses"] += 1
return None
# 检查是否过期双重检查防止TTL未及时清理
current_time = time.time()
if current_time > affinity_dict["expire_at"]:
await self._delete_affinity_key(cache_key)
self._stats["cache_misses"] += 1
return None
self._stats["cache_hits"] += 1
return CacheAffinity(
provider_id=affinity_dict["provider_id"],
endpoint_id=affinity_dict["endpoint_id"],
key_id=affinity_dict["key_id"],
api_format=affinity_dict.get("api_format", api_format),
model_name=affinity_dict.get("model_name", model_name),
created_at=affinity_dict["created_at"],
expire_at=affinity_dict["expire_at"],
request_count=affinity_dict["request_count"],
)
except Exception as e:
logger.exception(f"获取缓存亲和性失败: {e}")
self._stats["cache_misses"] += 1
return None
async def set_affinity(
self,
affinity_key: str,
provider_id: str,
endpoint_id: str,
key_id: str,
api_format: str,
model_name: str,
supports_caching: bool = True,
ttl: int | None = None,
) -> None:
"""
设置指定亲和性标识符对特定API格式和模型的缓存亲和性
Args:
affinity_key: 亲和性标识符通常为API Key ID
provider_id: Provider ID
endpoint_id: Endpoint ID
key_id: Key ID
api_format: API格式 (claude/openai)
model_name: 模型名称
supports_caching: 该Provider是否支持缓存
ttl: 缓存有效期(秒),如果不提供则使用默认值
注意每次调用都会刷新过期时间滑动窗口机制以保持对同一个Provider/Endpoint/Key的亲和性
"""
if not supports_caching:
# 不支持缓存的Provider不记录亲和性
logger.debug(f"Provider {provider_id[:8]}... 不支持缓存,跳过亲和性记录")
return
ttl = ttl or self.default_ttl
current_time = time.time()
expire_at = current_time + ttl # 每次都刷新过期时间
cache_key = self._get_cache_key(affinity_key, api_format, model_name)
try:
async with self._acquire_request_lock(cache_key):
existing_dict = await self._load_affinity_dict(cache_key)
existing_affinity: CacheAffinity | None = None
if existing_dict and current_time <= existing_dict.get("expire_at", 0):
existing_affinity = CacheAffinity(
provider_id=existing_dict["provider_id"],
endpoint_id=existing_dict["endpoint_id"],
key_id=existing_dict["key_id"],
api_format=existing_dict.get("api_format", api_format),
model_name=existing_dict.get("model_name", model_name),
created_at=existing_dict["created_at"],
expire_at=existing_dict["expire_at"],
request_count=existing_dict.get("request_count", 0),
)
if existing_affinity:
created_at = existing_affinity.created_at
request_count = existing_affinity.request_count + 1
# 检查是否切换了 Provider/Endpoint/Key
if (
existing_affinity.provider_id != provider_id
or existing_affinity.endpoint_id != endpoint_id
or existing_affinity.key_id != key_id
):
self._stats["key_switches"] += 1
logger.debug(
f"Key {affinity_key[:8]}... 在 {api_format} 格式下切换后端: "
f"[{existing_affinity.provider_id[:8]}.../{existing_affinity.endpoint_id[:8]}.../"
f"{existing_affinity.key_id[:8]}...] → "
f"[{provider_id[:8]}.../{endpoint_id[:8]}.../{key_id[:8]}...], 重置计数器"
)
created_at = current_time
request_count = 1
else:
logger.debug(
f"刷新缓存亲和性: key={affinity_key[:8]}..., api_format={api_format}, "
f"provider={provider_id[:8]}..., endpoint={endpoint_id[:8]}..., "
f"provider_key={key_id[:8]}..., ttl+={ttl}s"
)
else:
created_at = current_time
request_count = 1
self._stats["total_affinities"] += 1
affinity_dict = {
"provider_id": provider_id,
"endpoint_id": endpoint_id,
"key_id": key_id,
"api_format": api_format,
"model_name": model_name,
"created_at": created_at,
"expire_at": expire_at,
"request_count": request_count,
}
await self._save_affinity_dict(cache_key, ttl, affinity_dict)
logger.debug(
f"设置缓存亲和性: key={affinity_key[:8]}..., api_format={api_format}, "
f"model={model_name}, provider={provider_id[:8]}..., endpoint={endpoint_id[:8]}..., "
f"provider_key={key_id[:8]}..., ttl={ttl}s"
)
except Exception as e:
logger.exception(f"设置缓存亲和性失败: {e}")
async def invalidate_affinity(
self,
affinity_key: str,
api_format: str,
model_name: str,
key_id: str | None = None,
provider_id: str | None = None,
endpoint_id: str | None = None,
) -> None:
"""
失效指定亲和性标识符对特定API格式和模型的缓存亲和性
Args:
affinity_key: 亲和性标识符通常为API Key ID
api_format: API格式 (claude/openai)
model_name: 模型名称
key_id: Provider Key ID可选如果提供则只在Key匹配时失效
provider_id: Provider ID可选如果提供则只在Provider匹配时失效
endpoint_id: Endpoint ID可选如果提供则只在Endpoint匹配时失效
"""
existing_affinity = await self.get_affinity(affinity_key, api_format, model_name)
if not existing_affinity:
return
# 检查是否匹配过滤条件
should_invalidate = True
if key_id and existing_affinity.key_id != key_id:
should_invalidate = False
if provider_id and existing_affinity.provider_id != provider_id:
should_invalidate = False
if endpoint_id and existing_affinity.endpoint_id != endpoint_id:
should_invalidate = False
if not should_invalidate:
logger.debug(
f"跳过失效: affinity_key={affinity_key[:8]}..., api_format={api_format}, "
f"model={model_name}, 过滤条件不匹配 (key={key_id}, provider={provider_id}, endpoint={endpoint_id})"
)
return
try:
cache_key = self._get_cache_key(affinity_key, api_format, model_name)
async with self._acquire_request_lock(cache_key):
await self._delete_affinity_key(cache_key)
self._stats["cache_invalidations"] += 1
logger.debug(
f"失效缓存亲和性: affinity_key={affinity_key[:8]}..., api_format={api_format}, "
f"model={model_name}, provider={existing_affinity.provider_id[:8]}..., "
f"endpoint={existing_affinity.endpoint_id[:8]}..., "
f"provider_key={existing_affinity.key_id[:8]}..."
)
except Exception as e:
logger.exception(f"删除缓存亲和性失败: {e}")
async def invalidate_all_for_provider(self, provider_id: str) -> int:
"""
失效所有与指定Provider相关的缓存亲和性
用途当Provider关闭缓存支持时调用
Args:
provider_id: Provider ID
Returns:
失效的亲和性数量
"""
try:
invalidated_count = 0
if not self._is_memory_backend():
cursor: int | str = 0
while True:
cursor, scan_keys = await self.redis.scan(
cursor=cursor,
match="cache_affinity:*",
count=self.REDIS_SCAN_BATCH_SIZE,
)
if scan_keys:
# Pipeline batch GET to reduce round-trips.
values = await self.redis.mget(scan_keys)
keys_to_delete: list[str] = []
for key, raw in zip(scan_keys, values):
if not raw:
continue
try:
data = json.loads(raw)
except Exception:
continue
if data.get("provider_id") == provider_id:
keys_to_delete.append(key)
if keys_to_delete:
deleted = await self._delete_redis_keys(keys_to_delete)
invalidated_count += deleted
self._stats["cache_invalidations"] += deleted
# Clear L1 for deleted keys.
for key in keys_to_delete:
await self._set_l1_entry(key, None)
if int(cursor) == 0:
break
else:
keys = list((await self._snapshot_memory_items()).keys())
for key in keys:
affinity_dict = await self._load_affinity_dict(key)
if not affinity_dict:
continue
if affinity_dict.get("provider_id") == provider_id:
await self._delete_affinity_key(key)
invalidated_count += 1
self._stats["cache_invalidations"] += 1
if invalidated_count > 0:
logger.debug(
f"批量失效Provider缓存亲和性: provider={provider_id[:8]}..., "
f"失效数量={invalidated_count}"
)
return invalidated_count
except Exception as e:
logger.exception(f"批量失效Provider缓存亲和性失败: {e}")
return 0
async def clear_all(self) -> int:
"""
清除所有缓存亲和性(管理功能)
Returns:
清除的数量
"""
try:
if not self._is_memory_backend():
count = await self._scan_delete_pattern("cache_affinity:*")
await self._clear_l1_entries_by_prefix("cache_affinity:")
if count:
logger.debug(f"清除所有Redis缓存亲和性: {count}")
return count
lock = self._get_memory_lock()
async with lock:
count = len(self._memory_store)
self._memory_store.clear()
await self._clear_l1_entries_by_prefix("cache_affinity:")
if count:
logger.debug(f"清除所有内存缓存亲和性: {count}")
return count
except Exception as e:
logger.exception(f"清除缓存亲和性失败: {e}")
return 0
def get_stats(self) -> dict[str, Any]:
"""获取统计信息"""
cache_hit_rate = 0.0
total_requests = self._stats["cache_hits"] + self._stats["cache_misses"]
if total_requests > 0:
cache_hit_rate = self._stats["cache_hits"] / total_requests
storage_type = "redis" if not self._is_memory_backend() else "memory"
return {
"storage_type": storage_type,
"total_affinities": self._stats["total_affinities"],
"cache_hits": self._stats["cache_hits"],
"cache_misses": self._stats["cache_misses"],
"cache_hit_rate": cache_hit_rate,
"cache_invalidations": self._stats["cache_invalidations"],
"provider_switches": self._stats["provider_switches"],
"key_switches": self._stats["key_switches"],
"config": {
"default_ttl": self.default_ttl,
},
}
async def list_affinities(self) -> list[dict[str, Any]]:
"""获取所有缓存亲和性列表
返回的每条记录包含:
- affinity_key: 亲和性标识符(通常是 API Key ID
- provider_id, endpoint_id, key_id: Provider 相关信息
- api_format, model_name: API 格式和模型名称
- created_at, expire_at, request_count: 缓存元数据
"""
results: list[dict[str, Any]] = []
try:
pattern = "cache_affinity:*"
cursor = 0
if not self._is_memory_backend():
while True:
cursor, keys = await self.redis.scan(cursor=cursor, match=pattern, count=200)
if keys:
values = await self.redis.mget(*keys)
for cache_key, data in zip(keys, values):
if not data:
continue
try:
affinity = json.loads(data)
# 解析 cache_affinity:{affinity_key}:{api_format}:{model_name}
parts = cache_key.split(":")
affinity_key_value = parts[1] if len(parts) > 1 else cache_key
api_format = (
parts[2]
if len(parts) > 2
else affinity.get("api_format", "unknown")
)
model_name = (
parts[3]
if len(parts) > 3
else affinity.get("model_name", "unknown")
)
affinity["affinity_key"] = affinity_key_value
if "api_format" not in affinity:
affinity["api_format"] = api_format
if "model_name" not in affinity:
affinity["model_name"] = model_name
results.append(affinity)
except json.JSONDecodeError as e:
logger.exception(f"解析缓存亲和性记录失败: {cache_key} - {e}")
if cursor == 0:
break
else:
snapshot = await self._snapshot_memory_items()
expired_keys: list[str] = []
current_time = time.time()
for cache_key, affinity in snapshot.items():
if current_time > affinity["expire_at"]:
expired_keys.append(cache_key)
continue
# 解析 cache_affinity:{affinity_key}:{api_format}:{model_name}
parts = cache_key.split(":")
affinity_key_value = parts[1] if len(parts) > 1 else cache_key
api_format = (
parts[2] if len(parts) > 2 else affinity.get("api_format", "unknown")
)
model_name = (
parts[3] if len(parts) > 3 else affinity.get("model_name", "unknown")
)
affinity_with_key = dict(affinity)
affinity_with_key["affinity_key"] = affinity_key_value
if "api_format" not in affinity_with_key:
affinity_with_key["api_format"] = api_format
if "model_name" not in affinity_with_key:
affinity_with_key["model_name"] = model_name
results.append(affinity_with_key)
# 清理过期的键
if expired_keys:
async with self._get_memory_lock():
for key in expired_keys:
self._memory_store.pop(key, None)
except Exception as e:
logger.exception(f"获取缓存亲和性列表失败: {e}")
return results
# 全局单例
_affinity_manager: CacheAffinityManager | None = None
async def get_affinity_manager(redis_client: Any | None = None) -> CacheAffinityManager:
"""
获取全局CacheAffinityManager实例若Redis不可用则降级为内存模式
Args:
redis_client: Redis客户端可选
Returns:
CacheAffinityManager实例
"""
global _affinity_manager
if _affinity_manager is None:
_affinity_manager = CacheAffinityManager(redis_client)
elif redis_client and _affinity_manager.redis is None:
# 当最初使用内存后 Redis 可用时,升级为 Redis 存储
_affinity_manager = CacheAffinityManager(redis_client)
return _affinity_manager