From 0b2bd87d61189de442d0c76d9caef573ad0b0655 Mon Sep 17 00:00:00 2001 From: "34047007@qq.com" <34047007@qq.com> Date: Mon, 27 Jul 2026 16:16:21 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=A0=87=E7=AD=BE=E4=BA=8C=E7=BA=A7?= =?UTF-8?q?=E7=BC=93=E5=AD=98=20+=20cache.mget=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tag_loader 增加 Redis L2 缓存(tags:{lit_id}, 86400s TTL),批量 mget - cache.py 新增 mget() 方法,同键 Redis 批量查询 + memory 降级 - tag_service.py 打标后自动清除对应缓存 --- backend/app/core/cache.py | 12 ++++++ backend/app/services/tag_loader.py | 68 +++++++++++++++++++++--------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/backend/app/core/cache.py b/backend/app/core/cache.py index 1226ac6..3ed37c6 100644 --- a/backend/app/core/cache.py +++ b/backend/app/core/cache.py @@ -66,6 +66,18 @@ class CacheService: self._store.popitem(last=False) 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): r = await self._get_redis() if r: diff --git a/backend/app/services/tag_loader.py b/backend/app/services/tag_loader.py index 11a476d..98c0285 100644 --- a/backend/app/services/tag_loader.py +++ b/backend/app/services/tag_loader.py @@ -1,40 +1,68 @@ -"""共享标签加载工具(带单次请求级内存缓存)""" +"""共享标签加载工具(三级缓存:请求级内存 → Redis → DB)""" from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.core.cache import cache as _redis_cache from app.models.literature import GlobalLiteratureTag, GlobalTag -# 请求级缓存:同一批 literature_ids 在短时间内重复查询不走 DB +# 请求级缓存:同一批 literature_ids 在短时间内重复查询不走 DB/Redis _cache: dict[str, dict[str, list[dict]]] = {} _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]]: - """批量加载文献标签,返回 {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: 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: return _cache[cache_key] - import uuid as _uuid - uids = [_uuid.UUID(x) if isinstance(x, str) else x for x in literature_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_(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}) + # L2: Redis 单文献缓存(批量查) + redis_keys = [f"tags:{sid}" for sid in str_ids] + cached_results = await _redis_cache.mget(redis_keys) - # 存入请求级缓存,限制大小 + 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: _cache.pop(next(iter(_cache)), None) _cache[cache_key] = tags_map