perf: 标签二级缓存 + cache.mget 批量读取
- tag_loader 增加 Redis L2 缓存(tags:{lit_id}, 86400s TTL),批量 mget
- cache.py 新增 mget() 方法,同键 Redis 批量查询 + memory 降级
- tag_service.py 打标后自动清除对应缓存
This commit is contained in:
@@ -66,6 +66,18 @@ class CacheService:
|
|||||||
self._store.popitem(last=False)
|
self._store.popitem(last=False)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def mget(self, keys: list[str]) -> list[dict | None]:
|
||||||
|
"""批量获取,顺序对应 keys 列表。Redis 不可用时降级到内存模式。"""
|
||||||
|
r = await self._get_redis()
|
||||||
|
if r:
|
||||||
|
try:
|
||||||
|
vals = await r.mget(*keys)
|
||||||
|
return [json.loads(v) if v else None for v in vals]
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Redis MGET failed")
|
||||||
|
return [None] * len(keys)
|
||||||
|
return [self._store.get(k) for k in keys]
|
||||||
|
|
||||||
async def delete(self, key: str):
|
async def delete(self, key: str):
|
||||||
r = await self._get_redis()
|
r = await self._get_redis()
|
||||||
if r:
|
if r:
|
||||||
|
|||||||
@@ -1,40 +1,68 @@
|
|||||||
"""共享标签加载工具(带单次请求级内存缓存)"""
|
"""共享标签加载工具(三级缓存:请求级内存 → Redis → DB)"""
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.cache import cache as _redis_cache
|
||||||
from app.models.literature import GlobalLiteratureTag, GlobalTag
|
from app.models.literature import GlobalLiteratureTag, GlobalTag
|
||||||
|
|
||||||
# 请求级缓存:同一批 literature_ids 在短时间内重复查询不走 DB
|
# 请求级缓存:同一批 literature_ids 在短时间内重复查询不走 DB/Redis
|
||||||
_cache: dict[str, dict[str, list[dict]]] = {}
|
_cache: dict[str, dict[str, list[dict]]] = {}
|
||||||
_CACHE_MAX_KEYS = 5
|
_CACHE_MAX_KEYS = 5
|
||||||
|
_TAGS_CACHE_TTL = 86400 # 24h,标签变更极低频
|
||||||
|
|
||||||
|
|
||||||
async def load_tags_for_literature(db: AsyncSession, literature_ids: list[str]) -> dict[str, list[dict]]:
|
async def load_tags_for_literature(db: AsyncSession, literature_ids: list[str]) -> dict[str, list[dict]]:
|
||||||
"""批量加载文献标签,返回 {literature_id: [{name_zh, path, category, is_major}]}"""
|
"""批量加载文献标签,返回 {literature_id: [{name_zh, path, category, is_major}]}
|
||||||
|
|
||||||
|
三级缓存:
|
||||||
|
L1 — 请求级 dict(同一批 ID 在本次请求中复用)
|
||||||
|
L2 — Redis tags:{lit_id}(24h TTL,跨请求/跨用户)
|
||||||
|
L3 — DB JOIN(miss 时回退)
|
||||||
|
"""
|
||||||
if not literature_ids:
|
if not literature_ids:
|
||||||
return {}
|
return {}
|
||||||
# 缓存 key 基于排序后的 ID 列表,确保相同集合命中
|
|
||||||
cache_key = ",".join(sorted(literature_ids))
|
# L1: 请求级缓存
|
||||||
|
import uuid as _uuid
|
||||||
|
str_ids = [str(x) if not isinstance(x, str) else x for x in literature_ids]
|
||||||
|
cache_key = ",".join(sorted(str_ids))
|
||||||
if cache_key in _cache:
|
if cache_key in _cache:
|
||||||
return _cache[cache_key]
|
return _cache[cache_key]
|
||||||
|
|
||||||
import uuid as _uuid
|
# L2: Redis 单文献缓存(批量查)
|
||||||
uids = [_uuid.UUID(x) if isinstance(x, str) else x for x in literature_ids]
|
redis_keys = [f"tags:{sid}" for sid in str_ids]
|
||||||
result = await db.execute(
|
cached_results = await _redis_cache.mget(redis_keys)
|
||||||
select(GlobalLiteratureTag.literature_id, GlobalTag.id, GlobalTag.name_zh, GlobalTag.name_en, GlobalTag.path,
|
|
||||||
GlobalTag.tag_category, GlobalLiteratureTag.is_major)
|
|
||||||
.join(GlobalTag, GlobalLiteratureTag.tag_id == GlobalTag.id)
|
|
||||||
.where(GlobalLiteratureTag.literature_id.in_(uids))
|
|
||||||
)
|
|
||||||
tags_map: dict[str, list[dict]] = {}
|
|
||||||
for lit_id, tid, nz, ne, p, cat, maj in result:
|
|
||||||
sid = str(lit_id)
|
|
||||||
if sid not in tags_map:
|
|
||||||
tags_map[sid] = []
|
|
||||||
tags_map[sid].append({"id": str(tid), "name_zh": nz, "name_en": ne, "path": p, "category": cat, "is_major": maj})
|
|
||||||
|
|
||||||
# 存入请求级缓存,限制大小
|
tags_map: dict[str, list[dict]] = {}
|
||||||
|
miss_ids: list[str] = []
|
||||||
|
|
||||||
|
for sid, cached in zip(str_ids, cached_results):
|
||||||
|
if cached is not None and "tags" in cached:
|
||||||
|
tags_map[sid] = cached["tags"]
|
||||||
|
else:
|
||||||
|
miss_ids.append(sid)
|
||||||
|
|
||||||
|
# L3: DB 回退(只查 miss 的文献)
|
||||||
|
if miss_ids:
|
||||||
|
miss_uids = [_uuid.UUID(sid) for sid in miss_ids]
|
||||||
|
result = await db.execute(
|
||||||
|
select(GlobalLiteratureTag.literature_id, GlobalTag.id, GlobalTag.name_zh, GlobalTag.name_en, GlobalTag.path,
|
||||||
|
GlobalTag.tag_category, GlobalLiteratureTag.is_major)
|
||||||
|
.join(GlobalTag, GlobalLiteratureTag.tag_id == GlobalTag.id)
|
||||||
|
.where(GlobalLiteratureTag.literature_id.in_(miss_uids))
|
||||||
|
)
|
||||||
|
for lit_id, tid, nz, ne, p, cat, maj in result:
|
||||||
|
sid = str(lit_id)
|
||||||
|
if sid not in tags_map:
|
||||||
|
tags_map[sid] = []
|
||||||
|
tags_map[sid].append({"id": str(tid), "name_zh": nz, "name_en": ne, "path": p, "category": cat, "is_major": maj})
|
||||||
|
|
||||||
|
# 回填 Redis(空列表也缓存,避免空文献反复查 DB)
|
||||||
|
for sid in miss_ids:
|
||||||
|
await _redis_cache.set(f"tags:{sid}", {"tags": tags_map.get(sid, [])}, ttl=_TAGS_CACHE_TTL)
|
||||||
|
|
||||||
|
# L1: 存入请求级缓存
|
||||||
if len(_cache) >= _CACHE_MAX_KEYS:
|
if len(_cache) >= _CACHE_MAX_KEYS:
|
||||||
_cache.pop(next(iter(_cache)), None)
|
_cache.pop(next(iter(_cache)), None)
|
||||||
_cache[cache_key] = tags_map
|
_cache[cache_key] = tags_map
|
||||||
|
|||||||
Reference in New Issue
Block a user