Files
server-core/app/infrastructure/cache.py
T

197 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""基础设施层 · Redis 异步封装(缓存 / 限流 / 分布式锁)。
提供:
- 基础 get/set/delete/incr
- JSON 序列化 get_json/set_json
- 缓存装饰器 @cached(自动缓存函数返回值)
- 按前缀批量删除 delete_prefix
- 分布式锁 lock/unlock
- 全局单例 cache(应用启动时 connect,关闭时 close
"""
from __future__ import annotations
import asyncio
import functools
import json
import logging
from typing import Any, Callable, Optional
from redis.asyncio import Redis
from .. import config
logger = logging.getLogger(__name__)
class Cache:
"""redis.asyncio 封装:连接池 / get / set / incr / 分布式锁(SET NX EX)。"""
def __init__(self, url: str | None = None) -> None:
self._redis: Optional[Redis] = None
self._url = url or config.REDIS_URL
self._enabled = config.REDIS_ENABLED
async def connect(self) -> None:
if not self._enabled:
logger.info("Redis 未配置,缓存降级为内存(无缓存)")
return
if self._redis is None:
try:
self._redis = Redis.from_url(
self._url,
decode_responses=True,
max_connections=50,
socket_timeout=5.0,
socket_connect_timeout=5.0,
)
# 测试连接
await self._redis.ping()
logger.info(f"Redis 连接成功: {self._url}")
except Exception as e:
logger.warning(f"Redis 连接失败,缓存降级: {e}")
self._redis = None
async def close(self) -> None:
if self._redis is not None:
await self._redis.aclose()
self._redis = None
@property
def available(self) -> bool:
return self._redis is not None
# ── 基础字符串操作 ──────────────────────────────────────────────
async def get(self, key: str) -> str | None:
if self._redis is None:
return None
try:
return await self._redis.get(key)
except Exception as e:
logger.warning(f"Redis get 失败: {key}, {e}")
return None
async def set(self, key: str, value: str, ttl: int | None = None) -> None:
if self._redis is None:
return
try:
await self._redis.set(key, value, ex=ttl)
except Exception as e:
logger.warning(f"Redis set 失败: {key}, {e}")
async def delete(self, key: str) -> None:
if self._redis is None:
return
try:
await self._redis.delete(key)
except Exception as e:
logger.warning(f"Redis delete 失败: {key}, {e}")
async def incr(self, key: str) -> int:
if self._redis is None:
return 0
try:
return int(await self._redis.incr(key))
except Exception as e:
logger.warning(f"Redis incr 失败: {key}, {e}")
return 0
# ── JSON 序列化操作 ──────────────────────────────────────────────
async def get_json(self, key: str) -> Any | None:
"""获取并反序列化 JSON 值。"""
raw = await self.get(key)
if raw is None:
return None
try:
return json.loads(raw)
except Exception as e:
logger.warning(f"Redis JSON 反序列化失败: {key}, {e}")
return None
async def set_json(self, key: str, value: Any, ttl: int | None = None) -> None:
"""序列化并存储 JSON 值。"""
try:
raw = json.dumps(value, ensure_ascii=False, default=str)
except Exception as e:
logger.warning(f"Redis JSON 序列化失败: {key}, {e}")
return
await self.set(key, raw, ttl=ttl)
# ── 批量操作 ─────────────────────────────────────────────────────
async def delete_prefix(self, prefix: str) -> int:
"""按前缀批量删除 key,返回删除数量。"""
if self._redis is None:
return 0
try:
count = 0
async for key in self._redis.scan_iter(match=f"{prefix}*", count=100):
await self._redis.delete(key)
count += 1
return count
except Exception as e:
logger.warning(f"Redis delete_prefix 失败: {prefix}, {e}")
return 0
# ── 分布式锁 ─────────────────────────────────────────────────────
async def lock(self, key: str, ttl: int = 30) -> bool:
"""分布式锁:SET key token NX EX ttl(成功拿到锁返回 True)。"""
if self._redis is None:
return True # 无 Redis 时降级为总是通过(单机部署)
token = f"{asyncio.get_event_loop().time()}"
try:
ok = await self._redis.set(key, token, nx=True, ex=ttl)
return bool(ok)
except Exception as e:
logger.warning(f"Redis lock 失败: {key}, {e}")
return True
async def unlock(self, key: str) -> None:
if self._redis is None:
return
try:
await self._redis.delete(key)
except Exception as e:
logger.warning(f"Redis unlock 失败: {key}, {e}")
# ── 全局单例 ──────────────────────────────────────────────────────────
cache = Cache()
# ── 缓存装饰器 ────────────────────────────────────────────────────────
def cached(key_prefix: str, ttl: int = 60, *, include_args: bool = True) -> Callable:
"""异步函数缓存装饰器。
用法:
@cached("hall:tasks", ttl=10)
async def get_tasks(mode="", category=""):
...
key 格式:{key_prefix}:{args_hash}include_args=True 时)
写操作后需手动调用 cache.delete_prefix(key_prefix) 失效缓存。
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs):
if not cache.available:
return await func(*args, **kwargs)
# 构建缓存 key
if include_args:
import hashlib
arg_str = f"{args}:{sorted(kwargs.items())}"
arg_hash = hashlib.md5(arg_str.encode()).hexdigest()[:12]
cache_key = f"{key_prefix}:{arg_hash}"
else:
cache_key = key_prefix
# 尝试读缓存
cached_val = await cache.get_json(cache_key)
if cached_val is not None:
return cached_val
# 缓存未命中,执行函数
result = await func(*args, **kwargs)
# 写缓存(异步,不阻塞返回)
asyncio.create_task(cache.set_json(cache_key, result, ttl=ttl))
return result
return wrapper
return decorator