""" 缓存后端抽象层 提供统一的缓存接口,支持多种后端实现: 1. LocalCache: 内存缓存(单实例,线程安全) 2. RedisCache: Redis 缓存(分布式) 使用场景: - ModelCacheService: 模型解析缓存 - 其他需要缓存的服务 """ from __future__ import annotations import asyncio import json import time from abc import ABC, abstractmethod from collections import OrderedDict from typing import Any import redis.asyncio as aioredis from src.clients.redis_client import get_redis_client_sync from src.core.logger import logger class BaseCacheBackend(ABC): """缓存后端抽象基类""" @abstractmethod async def get(self, key: str) -> Any | None: """获取缓存值""" pass @abstractmethod async def set(self, key: str, value: Any, ttl: int = 300) -> None: """设置缓存值""" pass @abstractmethod async def delete(self, key: str) -> None: """删除缓存值""" pass @abstractmethod async def clear(self, pattern: str | None = None) -> None: """清空缓存(支持模式匹配)""" pass @abstractmethod async def exists(self, key: str) -> bool: """检查键是否存在""" pass class LocalCache(BaseCacheBackend): """本地内存缓存后端(LRU + TTL,线程安全)""" def __init__(self, max_size: int = 1000, default_ttl: int = 300): """ 初始化本地缓存 Args: max_size: 最大缓存条目数 default_ttl: 默认过期时间(秒) """ self._cache: OrderedDict = OrderedDict() self._expiry: dict[str, float] = {} self._max_size = max_size self._default_ttl = default_ttl self._lock = asyncio.Lock() async def get(self, key: str) -> Any | None: """获取缓存值(线程安全)""" async with self._lock: if key not in self._cache: return None # 检查过期 if key in self._expiry and time.time() > self._expiry[key]: # 过期,删除 del self._cache[key] del self._expiry[key] return None # 更新访问顺序(LRU) self._cache.move_to_end(key) return self._cache[key] async def set(self, key: str, value: Any, ttl: int | None = None) -> None: """设置缓存值(线程安全)""" async with self._lock: if ttl is None: ttl = self._default_ttl # 如果键已存在,更新访问顺序 if key in self._cache: self._cache.move_to_end(key) elif len(self._cache) >= self._max_size: # 插入新键前淘汰最旧项,确保容量不超过 max_size oldest_key = next(iter(self._cache)) del self._cache[oldest_key] if oldest_key in self._expiry: del self._expiry[oldest_key] self._cache[key] = value self._expiry[key] = time.time() + ttl async def delete(self, key: str) -> None: """删除缓存值(线程安全)""" async with self._lock: if key in self._cache: del self._cache[key] if key in self._expiry: del self._expiry[key] async def clear(self, pattern: str | None = None) -> None: """清空缓存(线程安全)""" async with self._lock: if pattern is None: # 清空所有 self._cache.clear() self._expiry.clear() else: # 模式匹配删除(简单实现:支持前缀匹配) prefix = pattern.rstrip("*") keys_to_delete = [k for k in self._cache.keys() if k.startswith(prefix)] for key in keys_to_delete: del self._cache[key] if key in self._expiry: del self._expiry[key] async def exists(self, key: str) -> bool: """检查键是否存在(线程安全)""" async with self._lock: if key not in self._cache: return False # 检查过期 if key in self._expiry and time.time() > self._expiry[key]: del self._cache[key] del self._expiry[key] return False return True def get_stats(self) -> dict[str, Any]: """获取缓存统计信息""" return { "backend": "local", "size": len(self._cache), "max_size": self._max_size, "default_ttl": self._default_ttl, } class RedisCache(BaseCacheBackend): """Redis 缓存后端(分布式)""" def __init__( self, redis_client: aioredis.Redis, key_prefix: str = "cache", default_ttl: int = 300 ): """ 初始化 Redis 缓存 Args: redis_client: Redis 客户端实例 key_prefix: 缓存键前缀 default_ttl: 默认过期时间(秒) """ self._redis = redis_client self._key_prefix = key_prefix self._default_ttl = default_ttl def _make_key(self, key: str) -> str: """构造完整的 Redis 键""" return f"{self._key_prefix}:{key}" async def get(self, key: str) -> Any | None: """获取缓存值""" try: redis_key = self._make_key(key) value = await self._redis.get(redis_key) if value is None: return None # 尝试 JSON 反序列化 try: return json.loads(value) except (json.JSONDecodeError, TypeError): # 如果不是 JSON,直接返回字符串 return value except Exception as e: logger.error(f"[RedisCache] 获取缓存失败: {key}, 错误: {e}") return None async def set(self, key: str, value: Any, ttl: int | None = None) -> None: """设置缓存值""" if ttl is None: ttl = self._default_ttl try: redis_key = self._make_key(key) # 序列化值 if isinstance(value, (dict, list, tuple)): serialized = json.dumps(value) elif isinstance(value, (int, float, bool)): serialized = json.dumps(value) else: serialized = str(value) await self._redis.setex(redis_key, ttl, serialized) except Exception as e: logger.error(f"[RedisCache] 设置缓存失败: {key}, 错误: {e}") async def delete(self, key: str) -> None: """删除缓存值""" try: redis_key = self._make_key(key) await self._redis.delete(redis_key) except Exception as e: logger.error(f"[RedisCache] 删除缓存失败: {key}, 错误: {e}") async def clear(self, pattern: str | None = None) -> None: """清空缓存""" try: if pattern is None: # 清空所有带前缀的键 pattern = "*" redis_pattern = self._make_key(pattern) cursor = 0 deleted_count = 0 while True: cursor, keys = await self._redis.scan(cursor, match=redis_pattern, count=100) if keys: await self._redis.delete(*keys) deleted_count += len(keys) if cursor == 0: break logger.info(f"[RedisCache] 清空缓存: {redis_pattern}, 删除 {deleted_count} 个键") except Exception as e: logger.error(f"[RedisCache] 清空缓存失败: {pattern}, 错误: {e}") async def exists(self, key: str) -> bool: """检查键是否存在""" try: redis_key = self._make_key(key) return await self._redis.exists(redis_key) > 0 except Exception as e: logger.error(f"[RedisCache] 检查键存在失败: {key}, 错误: {e}") return False async def publish_invalidation(self, channel: str, key: str) -> None: """发布缓存失效消息(用于分布式同步)""" try: message = json.dumps({"key": key, "timestamp": time.time()}) await self._redis.publish(channel, message) logger.debug(f"[RedisCache] 发布缓存失效: {channel} -> {key}") except Exception as e: logger.error(f"[RedisCache] 发布缓存失效失败: {channel}, {key}, 错误: {e}") def get_stats(self) -> dict[str, Any]: """获取缓存统计信息""" return { "backend": "redis", "key_prefix": self._key_prefix, "default_ttl": self._default_ttl, } # 缓存后端工厂 _cache_backends: dict[str, BaseCacheBackend] = {} _cache_backend_lock = asyncio.Lock() async def get_cache_backend( name: str, backend_type: str = "auto", max_size: int = 1000, ttl: int = 300 ) -> BaseCacheBackend: """ 获取缓存后端实例 Args: name: 缓存名称(用于区分不同的缓存实例) backend_type: 后端类型 (auto/local/redis) max_size: LocalCache 的最大容量 ttl: 默认过期时间(秒) Returns: BaseCacheBackend 实例 """ cache_key = f"{name}:{backend_type}" # 无锁快路径 if cache_key in _cache_backends: return _cache_backends[cache_key] async with _cache_backend_lock: # Double-check: 锁内再检查一次,避免重复创建 if cache_key in _cache_backends: return _cache_backends[cache_key] backend = _create_cache_backend(name, backend_type, max_size, ttl) _cache_backends[cache_key] = backend return backend def _create_cache_backend( name: str, backend_type: str, max_size: int, ttl: int ) -> BaseCacheBackend: """根据类型创建缓存后端实例""" if backend_type == "redis": redis_client = get_redis_client_sync() if redis_client is None: logger.warning(f"[CacheBackend] Redis 未初始化,{name} 降级为本地缓存") return LocalCache(max_size=max_size, default_ttl=ttl) else: logger.info(f"[CacheBackend] {name} 使用 Redis 缓存") return RedisCache(redis_client=redis_client, key_prefix=name, default_ttl=ttl) elif backend_type == "local": logger.info(f"[CacheBackend] {name} 使用本地缓存") return LocalCache(max_size=max_size, default_ttl=ttl) else: # auto redis_client = get_redis_client_sync() if redis_client is not None: logger.debug(f"[CacheBackend] {name} 自动选择 Redis 缓存") return RedisCache(redis_client=redis_client, key_prefix=name, default_ttl=ttl) else: logger.debug(f"[CacheBackend] {name} 自动选择本地缓存(Redis 不可用)") return LocalCache(max_size=max_size, default_ttl=ttl)