first commit

This commit is contained in:
2026-06-25 17:41:06 +08:00
commit b2f23a933b
370 changed files with 30526 additions and 0 deletions
+163
View File
@@ -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 返回 bytesstdlib 返回 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