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:
34047007@qq.com
2026-07-27 16:16:21 +08:00
parent c791c06096
commit 0b2bd87d61
2 changed files with 60 additions and 20 deletions
+12
View File
@@ -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:
+48 -20
View File
@@ -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 JOINmiss 时回退)
"""
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