添加谱面查询缓存
This commit is contained in:
16
app/dependencies/beatmapset_cache.py
Normal file
16
app/dependencies/beatmapset_cache.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Beatmapset缓存服务依赖注入
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.dependencies.database import get_redis
|
||||||
|
from app.service.beatmapset_cache_service import BeatmapsetCacheService, get_beatmapset_cache_service
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
|
|
||||||
|
def get_beatmapset_cache_dependency(redis: Redis = Depends(get_redis)) -> BeatmapsetCacheService:
|
||||||
|
"""获取beatmapset缓存服务依赖"""
|
||||||
|
return get_beatmapset_cache_service(redis)
|
||||||
@@ -7,6 +7,7 @@ from urllib.parse import parse_qs
|
|||||||
from app.database import Beatmap, Beatmapset, BeatmapsetResp, FavouriteBeatmapset, User
|
from app.database import Beatmap, Beatmapset, BeatmapsetResp, FavouriteBeatmapset, User
|
||||||
from app.database.beatmapset import SearchBeatmapsetsResp
|
from app.database.beatmapset import SearchBeatmapsetsResp
|
||||||
from app.dependencies.beatmap_download import get_beatmap_download_service
|
from app.dependencies.beatmap_download import get_beatmap_download_service
|
||||||
|
from app.dependencies.beatmapset_cache import get_beatmapset_cache_dependency
|
||||||
from app.dependencies.database import Database, get_redis, with_db
|
from app.dependencies.database import Database, get_redis, with_db
|
||||||
from app.dependencies.fetcher import get_fetcher
|
from app.dependencies.fetcher import get_fetcher
|
||||||
from app.dependencies.geoip import get_client_ip, get_geoip_helper
|
from app.dependencies.geoip import get_client_ip, get_geoip_helper
|
||||||
@@ -15,6 +16,7 @@ from app.fetcher import Fetcher
|
|||||||
from app.models.beatmap import SearchQueryModel
|
from app.models.beatmap import SearchQueryModel
|
||||||
from app.service.asset_proxy_helper import process_response_assets
|
from app.service.asset_proxy_helper import process_response_assets
|
||||||
from app.service.beatmap_download_service import BeatmapDownloadService
|
from app.service.beatmap_download_service import BeatmapDownloadService
|
||||||
|
from app.service.beatmapset_cache_service import BeatmapsetCacheService, generate_hash
|
||||||
|
|
||||||
from .router import router
|
from .router import router
|
||||||
|
|
||||||
@@ -54,6 +56,7 @@ async def search_beatmapset(
|
|||||||
current_user: User = Security(get_current_user, scopes=["public"]),
|
current_user: User = Security(get_current_user, scopes=["public"]),
|
||||||
fetcher: Fetcher = Depends(get_fetcher),
|
fetcher: Fetcher = Depends(get_fetcher),
|
||||||
redis=Depends(get_redis),
|
redis=Depends(get_redis),
|
||||||
|
cache_service: BeatmapsetCacheService = Depends(get_beatmapset_cache_dependency),
|
||||||
):
|
):
|
||||||
params = parse_qs(qs=request.url.query, keep_blank_values=True)
|
params = parse_qs(qs=request.url.query, keep_blank_values=True)
|
||||||
cursor = {}
|
cursor = {}
|
||||||
@@ -94,15 +97,31 @@ async def search_beatmapset(
|
|||||||
):
|
):
|
||||||
# TODO: search locally
|
# TODO: search locally
|
||||||
return SearchBeatmapsetsResp(total=0, beatmapsets=[])
|
return SearchBeatmapsetsResp(total=0, beatmapsets=[])
|
||||||
|
|
||||||
|
# 生成查询和游标的哈希用于缓存
|
||||||
|
query_hash = generate_hash(query.model_dump())
|
||||||
|
cursor_hash = generate_hash(cursor)
|
||||||
|
|
||||||
|
# 尝试从缓存获取搜索结果
|
||||||
|
cached_result = await cache_service.get_search_from_cache(query_hash, cursor_hash)
|
||||||
|
if cached_result:
|
||||||
|
sets = SearchBeatmapsetsResp(**cached_result)
|
||||||
|
# 处理资源代理
|
||||||
|
processed_sets = await process_response_assets(sets, request)
|
||||||
|
return processed_sets
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sets = await fetcher.search_beatmapset(query, cursor, redis)
|
sets = await fetcher.search_beatmapset(query, cursor, redis)
|
||||||
background_tasks.add_task(_save_to_db, sets)
|
background_tasks.add_task(_save_to_db, sets)
|
||||||
|
|
||||||
|
# 缓存搜索结果
|
||||||
|
await cache_service.cache_search_result(query_hash, cursor_hash, sets.model_dump())
|
||||||
|
|
||||||
# 处理资源代理
|
# 处理资源代理
|
||||||
processed_sets = await process_response_assets(sets, request)
|
processed_sets = await process_response_assets(sets, request)
|
||||||
return processed_sets
|
return processed_sets
|
||||||
except HTTPError as e:
|
except HTTPError as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -118,13 +137,27 @@ async def lookup_beatmapset(
|
|||||||
beatmap_id: int = Query(description="谱面 ID"),
|
beatmap_id: int = Query(description="谱面 ID"),
|
||||||
current_user: User = Security(get_current_user, scopes=["public"]),
|
current_user: User = Security(get_current_user, scopes=["public"]),
|
||||||
fetcher: Fetcher = Depends(get_fetcher),
|
fetcher: Fetcher = Depends(get_fetcher),
|
||||||
|
cache_service: BeatmapsetCacheService = Depends(get_beatmapset_cache_dependency),
|
||||||
):
|
):
|
||||||
beatmap = await Beatmap.get_or_fetch(db, fetcher, bid=beatmap_id)
|
# 先尝试从缓存获取
|
||||||
resp = await BeatmapsetResp.from_db(beatmap.beatmapset, session=db, user=current_user)
|
cached_resp = await cache_service.get_beatmap_lookup_from_cache(beatmap_id)
|
||||||
|
if cached_resp:
|
||||||
|
# 处理资源代理
|
||||||
|
processed_resp = await process_response_assets(cached_resp, request)
|
||||||
|
return processed_resp
|
||||||
|
|
||||||
# 处理资源代理
|
try:
|
||||||
processed_resp = await process_response_assets(resp, request)
|
beatmap = await Beatmap.get_or_fetch(db, fetcher, bid=beatmap_id)
|
||||||
return processed_resp
|
resp = await BeatmapsetResp.from_db(beatmap.beatmapset, session=db, user=current_user)
|
||||||
|
|
||||||
|
# 缓存结果
|
||||||
|
await cache_service.cache_beatmap_lookup(beatmap_id, resp)
|
||||||
|
|
||||||
|
# 处理资源代理
|
||||||
|
processed_resp = await process_response_assets(resp, request)
|
||||||
|
return processed_resp
|
||||||
|
except HTTPError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail="Beatmap not found") from exc
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -136,15 +169,31 @@ async def lookup_beatmapset(
|
|||||||
)
|
)
|
||||||
async def get_beatmapset(
|
async def get_beatmapset(
|
||||||
db: Database,
|
db: Database,
|
||||||
|
request: Request,
|
||||||
beatmapset_id: int = Path(..., description="谱面集 ID"),
|
beatmapset_id: int = Path(..., description="谱面集 ID"),
|
||||||
current_user: User = Security(get_current_user, scopes=["public"]),
|
current_user: User = Security(get_current_user, scopes=["public"]),
|
||||||
fetcher: Fetcher = Depends(get_fetcher),
|
fetcher: Fetcher = Depends(get_fetcher),
|
||||||
|
cache_service: BeatmapsetCacheService = Depends(get_beatmapset_cache_dependency),
|
||||||
):
|
):
|
||||||
|
# 先尝试从缓存获取
|
||||||
|
cached_resp = await cache_service.get_beatmapset_from_cache(beatmapset_id)
|
||||||
|
if cached_resp:
|
||||||
|
# 处理资源代理
|
||||||
|
processed_resp = await process_response_assets(cached_resp, request)
|
||||||
|
return processed_resp
|
||||||
|
|
||||||
try:
|
try:
|
||||||
beatmapset = await Beatmapset.get_or_fetch(db, fetcher, beatmapset_id)
|
beatmapset = await Beatmapset.get_or_fetch(db, fetcher, beatmapset_id)
|
||||||
return await BeatmapsetResp.from_db(beatmapset, session=db, include=["recent_favourites"], user=current_user)
|
resp = await BeatmapsetResp.from_db(beatmapset, session=db, include=["recent_favourites"], user=current_user)
|
||||||
except HTTPError:
|
|
||||||
raise HTTPException(status_code=404, detail="Beatmapset not found")
|
# 缓存结果
|
||||||
|
await cache_service.cache_beatmapset(resp)
|
||||||
|
|
||||||
|
# 处理资源代理
|
||||||
|
processed_resp = await process_response_assets(resp, request)
|
||||||
|
return processed_resp
|
||||||
|
except HTTPError as exc:
|
||||||
|
raise HTTPException(status_code=404, detail="Beatmapset not found") from exc
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
|
|||||||
205
app/service/beatmapset_cache_service.py
Normal file
205
app/service/beatmapset_cache_service.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"""
|
||||||
|
Beatmapset缓存服务
|
||||||
|
用于缓存beatmapset数据,减少数据库查询频率
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.database.beatmapset import BeatmapsetResp
|
||||||
|
from app.log import logger
|
||||||
|
|
||||||
|
from redis.asyncio import Redis
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.fetcher import Fetcher
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
"""处理datetime序列化的JSON编码器"""
|
||||||
|
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_json_dumps(data) -> str:
|
||||||
|
"""安全的JSON序列化,处理datetime对象"""
|
||||||
|
return json.dumps(data, cls=DateTimeEncoder, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_hash(data) -> str:
|
||||||
|
"""生成数据的MD5哈希值"""
|
||||||
|
if isinstance(data, str):
|
||||||
|
content = data
|
||||||
|
else:
|
||||||
|
content = safe_json_dumps(data)
|
||||||
|
return hashlib.md5(content.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class BeatmapsetCacheService:
|
||||||
|
"""Beatmapset缓存服务"""
|
||||||
|
|
||||||
|
def __init__(self, redis: Redis):
|
||||||
|
self.redis = redis
|
||||||
|
self._default_ttl = getattr(settings, "beatmapset_cache_expire_seconds", 3600) # 1小时默认TTL
|
||||||
|
|
||||||
|
def _get_beatmapset_cache_key(self, beatmapset_id: int) -> str:
|
||||||
|
"""生成beatmapset缓存键"""
|
||||||
|
return f"beatmapset:{beatmapset_id}"
|
||||||
|
|
||||||
|
def _get_beatmap_lookup_cache_key(self, beatmap_id: int) -> str:
|
||||||
|
"""生成beatmap lookup缓存键"""
|
||||||
|
return f"beatmap_lookup:{beatmap_id}:beatmapset"
|
||||||
|
|
||||||
|
def _get_search_cache_key(self, query_hash: str, cursor_hash: str) -> str:
|
||||||
|
"""生成搜索结果缓存键"""
|
||||||
|
return f"beatmapset_search:{query_hash}:{cursor_hash}"
|
||||||
|
|
||||||
|
async def get_beatmapset_from_cache(self, beatmapset_id: int) -> BeatmapsetResp | None:
|
||||||
|
"""从缓存获取beatmapset信息"""
|
||||||
|
try:
|
||||||
|
cache_key = self._get_beatmapset_cache_key(beatmapset_id)
|
||||||
|
cached_data = await self.redis.get(cache_key)
|
||||||
|
if cached_data:
|
||||||
|
logger.debug(f"Beatmapset cache hit for {beatmapset_id}")
|
||||||
|
data = json.loads(cached_data)
|
||||||
|
return BeatmapsetResp(**data)
|
||||||
|
return None
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error getting beatmapset from cache: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cache_beatmapset(
|
||||||
|
self,
|
||||||
|
beatmapset_resp: BeatmapsetResp,
|
||||||
|
expire_seconds: int | None = None,
|
||||||
|
):
|
||||||
|
"""缓存beatmapset信息"""
|
||||||
|
try:
|
||||||
|
if expire_seconds is None:
|
||||||
|
expire_seconds = self._default_ttl
|
||||||
|
if beatmapset_resp.id is None:
|
||||||
|
logger.warning("Cannot cache beatmapset with None id")
|
||||||
|
return
|
||||||
|
cache_key = self._get_beatmapset_cache_key(beatmapset_resp.id)
|
||||||
|
cached_data = beatmapset_resp.model_dump_json()
|
||||||
|
await self.redis.setex(cache_key, expire_seconds, cached_data) # type: ignore
|
||||||
|
logger.debug(f"Cached beatmapset {beatmapset_resp.id} for {expire_seconds}s")
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error caching beatmapset: {e}")
|
||||||
|
|
||||||
|
async def get_beatmap_lookup_from_cache(self, beatmap_id: int) -> BeatmapsetResp | None:
|
||||||
|
"""从缓存获取通过beatmap ID查找的beatmapset信息"""
|
||||||
|
try:
|
||||||
|
cache_key = self._get_beatmap_lookup_cache_key(beatmap_id)
|
||||||
|
cached_data = await self.redis.get(cache_key)
|
||||||
|
if cached_data:
|
||||||
|
logger.debug(f"Beatmap lookup cache hit for {beatmap_id}")
|
||||||
|
data = json.loads(cached_data)
|
||||||
|
return BeatmapsetResp(**data)
|
||||||
|
return None
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error getting beatmap lookup from cache: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cache_beatmap_lookup(
|
||||||
|
self,
|
||||||
|
beatmap_id: int,
|
||||||
|
beatmapset_resp: BeatmapsetResp,
|
||||||
|
expire_seconds: int | None = None,
|
||||||
|
):
|
||||||
|
"""缓存通过beatmap ID查找的beatmapset信息"""
|
||||||
|
try:
|
||||||
|
if expire_seconds is None:
|
||||||
|
expire_seconds = self._default_ttl
|
||||||
|
cache_key = self._get_beatmap_lookup_cache_key(beatmap_id)
|
||||||
|
cached_data = beatmapset_resp.model_dump_json()
|
||||||
|
await self.redis.setex(cache_key, expire_seconds, cached_data) # type: ignore
|
||||||
|
logger.debug(f"Cached beatmap lookup {beatmap_id} for {expire_seconds}s")
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error caching beatmap lookup: {e}")
|
||||||
|
|
||||||
|
async def get_search_from_cache(self, query_hash: str, cursor_hash: str) -> dict | None:
|
||||||
|
"""从缓存获取搜索结果"""
|
||||||
|
try:
|
||||||
|
cache_key = self._get_search_cache_key(query_hash, cursor_hash)
|
||||||
|
cached_data = await self.redis.get(cache_key)
|
||||||
|
if cached_data:
|
||||||
|
logger.debug(f"Search cache hit for {query_hash[:8]}...{cursor_hash[:8]}")
|
||||||
|
return json.loads(cached_data)
|
||||||
|
return None
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error getting search from cache: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def cache_search_result(
|
||||||
|
self,
|
||||||
|
query_hash: str,
|
||||||
|
cursor_hash: str,
|
||||||
|
search_result: dict,
|
||||||
|
expire_seconds: int | None = None,
|
||||||
|
):
|
||||||
|
"""缓存搜索结果"""
|
||||||
|
try:
|
||||||
|
if expire_seconds is None:
|
||||||
|
expire_seconds = min(self._default_ttl, 300) # 搜索结果缓存时间较短,最多5分钟
|
||||||
|
cache_key = self._get_search_cache_key(query_hash, cursor_hash)
|
||||||
|
cached_data = safe_json_dumps(search_result)
|
||||||
|
await self.redis.setex(cache_key, expire_seconds, cached_data) # type: ignore
|
||||||
|
logger.debug(f"Cached search result for {expire_seconds}s")
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error caching search result: {e}")
|
||||||
|
|
||||||
|
async def invalidate_beatmapset_cache(self, beatmapset_id: int):
|
||||||
|
"""使beatmapset缓存失效"""
|
||||||
|
try:
|
||||||
|
cache_key = self._get_beatmapset_cache_key(beatmapset_id)
|
||||||
|
await self.redis.delete(cache_key)
|
||||||
|
logger.debug(f"Invalidated beatmapset cache for {beatmapset_id}")
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error invalidating beatmapset cache: {e}")
|
||||||
|
|
||||||
|
async def invalidate_beatmap_lookup_cache(self, beatmap_id: int):
|
||||||
|
"""使beatmap lookup缓存失效"""
|
||||||
|
try:
|
||||||
|
cache_key = self._get_beatmap_lookup_cache_key(beatmap_id)
|
||||||
|
await self.redis.delete(cache_key)
|
||||||
|
logger.debug(f"Invalidated beatmap lookup cache for {beatmap_id}")
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error invalidating beatmap lookup cache: {e}")
|
||||||
|
|
||||||
|
async def get_cache_stats(self) -> dict:
|
||||||
|
"""获取缓存统计信息"""
|
||||||
|
try:
|
||||||
|
beatmapset_keys = await self.redis.keys("beatmapset:*")
|
||||||
|
lookup_keys = await self.redis.keys("beatmap_lookup:*")
|
||||||
|
search_keys = await self.redis.keys("beatmapset_search:*")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cached_beatmapsets": len(beatmapset_keys),
|
||||||
|
"cached_lookups": len(lookup_keys),
|
||||||
|
"cached_searches": len(search_keys),
|
||||||
|
"total_keys": len(beatmapset_keys) + len(lookup_keys) + len(search_keys),
|
||||||
|
}
|
||||||
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
|
logger.error(f"Error getting cache stats: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
# 全局缓存服务实例
|
||||||
|
_cache_service: BeatmapsetCacheService | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_beatmapset_cache_service(redis: Redis) -> BeatmapsetCacheService:
|
||||||
|
"""获取beatmapset缓存服务实例"""
|
||||||
|
global _cache_service # noqa: PLW0603
|
||||||
|
if _cache_service is None:
|
||||||
|
_cache_service = BeatmapsetCacheService(redis)
|
||||||
|
return _cache_service
|
||||||
Reference in New Issue
Block a user