first commit
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user