"""Redis 缓存服务 为检索结果与 query 解析结果提供 JSON 缓存。所有操作容错: Redis 不可用或缓存数据异常时降级为未命中 / 写入失败,绝不影响主流程。 """ import json from typing import Any import structlog from redis import asyncio as redis_async from app.config import settings logger = structlog.get_logger() class RedisCache: """Redis JSON 缓存客户端(懒连接,全操作容错)""" def __init__(self) -> None: self._client: redis_async.Redis | None = None def _get_client(self) -> redis_async.Redis: """懒创建 Redis 客户端(TCP 连接在首次执行命令时才建立)""" if self._client is None: self._client = redis_async.from_url(settings.redis_url, decode_responses=True) return self._client async def get_json(self, key: str) -> dict[str, Any] | None: """读取缓存并反序列化为 dict 任何异常(连接失败 / JSON 解析失败)或值非 dict 时都降级为未命中,返回 None。 """ try: raw = await self._get_client().get(key) if raw is None: return None data = json.loads(raw) return data if isinstance(data, dict) else None except Exception: logger.warning("Redis 读取缓存失败,降级为未命中", key=key, exc_info=True) return None async def set_json(self, key: str, value: dict[str, Any], ttl: int | None = None) -> bool: """写入缓存(SETEX),ttl 缺省取 settings.cache_ttl 异常时记录日志并返回 False,不影响主流程。 """ try: await self._get_client().setex( key, ttl if ttl is not None else settings.cache_ttl, json.dumps(value, ensure_ascii=False) ) return True except Exception: logger.warning("Redis 写入缓存失败", key=key, exc_info=True) return False async def close(self) -> None: """关闭底层连接""" if self._client is not None: await self._client.aclose() self._client = None # 模块级懒加载单例,避免每请求重建客户端 _cache: RedisCache | None = None def get_cache() -> RedisCache: """获取全局 RedisCache 单例""" global _cache if _cache is None: _cache = RedisCache() return _cache