启用数据库预加载器,清理日志

This commit is contained in:
Windpicker-owo
2025-12-08 17:17:53 +08:00
parent 96ed5a6789
commit a1d60ab026
46 changed files with 484 additions and 886 deletions

View File

@@ -13,6 +13,7 @@ from collections import defaultdict
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any
from collections import OrderedDict
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -22,6 +23,15 @@ from src.common.logger import get_logger
logger = get_logger("preloader")
# 预加载注册表(用于后台刷新热点数据)
_preload_loader_registry: OrderedDict[str, Callable[[], Awaitable[Any]]] = OrderedDict()
_registry_lock = asyncio.Lock()
_preload_task: asyncio.Task | None = None
_preload_task_lock = asyncio.Lock()
_PRELOAD_REGISTRY_LIMIT = 1024
# 默认后台预加载轮询间隔(秒)
_DEFAULT_PRELOAD_INTERVAL = 60
@dataclass
class AccessPattern:
@@ -223,16 +233,19 @@ class DataPreloader:
async def start_preload_batch(
self,
session: AsyncSession,
loaders: dict[str, Callable[[], Awaitable[Any]]],
limit: int = 100,
) -> None:
"""批量启动预加载任务
Args:
session: 数据库会话
loaders: 数据键到加载函数的映射
limit: 参与预加载的热点键数量上限
"""
preload_keys = await self.get_preload_keys()
if not loaders:
return
preload_keys = await self.get_preload_keys(limit=limit)
for key in preload_keys:
if key in loaders:
@@ -418,6 +431,91 @@ class CommonDataPreloader:
await self.preloader.preload_data(cache_key, loader)
# 预加载后台任务与注册表管理
async def _get_preload_interval() -> float:
"""获取后台预加载轮询间隔"""
try:
from src.config.config import global_config
if global_config and getattr(global_config, "database", None):
interval = getattr(global_config.database, "preload_interval", None)
if interval:
return max(5.0, float(interval))
except Exception:
# 配置可能未加载或不存在该字段,使用默认值
pass
return float(_DEFAULT_PRELOAD_INTERVAL)
async def _register_preload_loader(
cache_key: str,
loader: Callable[[], Awaitable[Any]],
) -> None:
"""注册用于热点预加载的加载函数"""
async with _registry_lock:
# move_to_end可以保持最近注册的顺序便于淘汰旧项
_preload_loader_registry[cache_key] = loader
_preload_loader_registry.move_to_end(cache_key)
# 控制注册表大小,避免无限增长
while len(_preload_loader_registry) > _PRELOAD_REGISTRY_LIMIT:
_preload_loader_registry.popitem(last=False)
async def _snapshot_loaders() -> dict[str, Callable[[], Awaitable[Any]]]:
"""获取当前注册的预加载loader快照"""
async with _registry_lock:
return dict(_preload_loader_registry)
async def _preload_worker() -> None:
"""后台周期性预加载任务"""
while True:
try:
interval = await _get_preload_interval()
loaders = await _snapshot_loaders()
if loaders:
preloader = await get_preloader()
await preloader.start_preload_batch(loaders)
await asyncio.sleep(interval)
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"预加载后台任务异常: {e}")
# 避免紧急重试导致CPU占用过高
await asyncio.sleep(5)
async def _ensure_preload_worker() -> None:
"""确保后台预加载任务已启动"""
global _preload_task
async with _preload_task_lock:
if _preload_task is None or _preload_task.done():
_preload_task = asyncio.create_task(_preload_worker())
async def record_preload_access(
cache_key: str,
*,
related_keys: list[str] | None = None,
loader: Callable[[], Awaitable[Any]] | None = None,
) -> None:
"""记录访问并注册预加载loader
这个入口为上层APICRUD/Query提供记录访问模式、建立关联关系、
以及注册用于后续后台预加载的加载函数。
"""
preloader = await get_preloader()
await preloader.record_access(cache_key, related_keys)
if loader is not None:
await _register_preload_loader(cache_key, loader)
await _ensure_preload_worker()
# 全局预加载器实例
_global_preloader: DataPreloader | None = None
_preloader_lock = asyncio.Lock()
@@ -438,7 +536,22 @@ async def get_preloader() -> DataPreloader:
async def close_preloader() -> None:
"""关闭全局预加载器"""
global _global_preloader
global _preload_task
# 停止后台任务
if _preload_task is not None:
_preload_task.cancel()
try:
await _preload_task
except asyncio.CancelledError:
pass
_preload_task = None
# 清理注册表
async with _registry_lock:
_preload_loader_registry.clear()
# 清理预加载器实例
if _global_preloader is not None:
await _global_preloader.clear()
_global_preloader = None