# core/cache.py import json as _stdlib_json from contextlib import asynccontextmanager from typing import Any, AsyncGenerator, Optional, Union import logging logger = logging.getLogger(__name__) from redis.asyncio import Redis # ===== 尝试使用你的 fff,否则回退到 stdlib ===== try: import orjson as json except ImportError: json = _stdlib_json # type: ignore # ===== Redis 客户端单例(可替换为你自己的)===== class RedisClient: _instance: Optional["RedisClient"] = None _redis: Optional[Redis] = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) return cls._instance async def init_redis(self, url: str = "redis://localhost:6379/0") -> None: if self._redis is None: self._redis = Redis.from_url(url, decode_responses=False) @property def client(self) -> Redis: if self._redis is None: raise RuntimeError("Redis not initialized. Call init_redis() first.") return self._redis async def close(self) -> None: if self._redis: await self._redis.close() self._redis = None redis_client = RedisClient() # ===== FastAPI 依赖注入 ===== async def get_redis() -> Redis: """FastAPI 依赖:获取 Redis 客户端""" return redis_client.client async def invalidate_cache(key: str) -> bool: """ 主动删除缓存键。 返回是否成功删除(Redis delete 返回被删除的 key 数量)。 """ try: result = await redis_client.client.delete(key) return result > 0 except Exception as e: logger.exception(f"Failed to invalidate cache key: {key}", exc_info=e) return False # ===== 缓存上下文管理器 ===== class CacheResult: __slots__ = ("key", "hit", "value", "_to_set", "_set_called") def __init__(self, key: str) -> None: self.key = key self.hit = False self.value: Any = None self._to_set: Any = None self._set_called = False def set(self, value: Any) -> None: """标记要缓存的值(可为 None)""" self._to_set = value self._set_called = True @asynccontextmanager async def cache_if( key: str, ttl: int = 3600, redis: Optional[Redis] = None, ) -> AsyncGenerator[CacheResult, None]: """ 异步缓存上下文管理器,支持 None 值缓存(防穿透)。 Args: key: 缓存键 ttl: 正常值缓存时间(秒) redis: 可选 Redis 客户端(用于测试或自定义) Usage: async with cache_if("report:123") as cache: if cache.hit: return cache.value result = await compute() cache.set(result) # result 可为 None """ result = CacheResult(key) client = redis or redis_client.client # 尝试读缓存 try: cached_val = await client.get(key) if cached_val is not None: # 解码 if cached_val == b"__NULL__": result.hit = True result.value = None else: result.hit = True result.value = json.loads(cached_val) except Exception: # Redis 不可用,降级(不中断主流程) logger.exception(f"Redis get error for key: {key}") pass yield result # 写缓存(仅当调用了 set()) if result._set_called: try: if result._to_set is None: val = b"__NULL__" ex = 60 # 空值短 TTL else: # 注意:orjson.dumps 返回 bytes,stdlib 返回 str → 统一转 bytes serialized = json.dumps(result._to_set) val = serialized if isinstance(serialized, bytes) else serialized.encode("utf-8") ex = ttl await client.setex(key, ex, val) except Exception: # 写缓存失败,不影响主流程 pass # ===== 装饰器版(可选补充)===== from functools import wraps import asyncio import hashlib def cached(ttl: int = 3600): """函数缓存装饰器(使用 cache_if)""" def decorator(func): @wraps(func) async def wrapper(*args, **kwargs): # 生成 key(简单版,可替换为更 robust 的) key_data = str(args) + str(sorted(kwargs.items())) key = f"cached:{func.__name__}:{hashlib.md5(key_data.encode()).hexdigest()}" logger.debug(f"Cache key: {key}") async with cache_if(key, ttl) as cache: if cache.hit: logger.debug(f"Cache hit for key: {key}") return cache.value result = await func(*args, **kwargs) cache.set(result) return result return wrapper return decorator