feat(cache): 添加 LRU 淘汰机制和缓存大小限制以优化内存使用

This commit is contained in:
Windpicker-owo
2025-12-03 11:42:38 +08:00
parent d5e6746a21
commit 1acead1f9d
4 changed files with 145 additions and 5 deletions

View File

@@ -437,7 +437,13 @@ class StyleLearner:
class StyleLearnerManager:
"""多聊天室表达风格学习管理器"""
"""多聊天室表达风格学习管理器
添加 LRU 淘汰机制,限制最大活跃 learner 数量
"""
# 🔧 最大活跃 learner 数量
MAX_ACTIVE_LEARNERS = 50
def __init__(self, model_save_path: str = "data/expression/style_models"):
"""
@@ -445,6 +451,7 @@ class StyleLearnerManager:
model_save_path: 模型保存路径
"""
self.learners: dict[str, StyleLearner] = {}
self.learner_last_used: dict[str, float] = {} # 🔧 记录最后使用时间
self.model_save_path = model_save_path
# 确保保存目录存在
@@ -452,6 +459,30 @@ class StyleLearnerManager:
logger.debug(f"StyleLearnerManager初始化成功, 模型保存路径: {model_save_path}")
def _evict_if_needed(self) -> None:
"""🔧 内存优化:如果超过最大数量,淘汰最久未使用的 learner"""
if len(self.learners) < self.MAX_ACTIVE_LEARNERS:
return
# 按最后使用时间排序,淘汰最旧的 20%
evict_count = max(1, len(self.learners) // 5)
sorted_by_time = sorted(
self.learner_last_used.items(),
key=lambda x: x[1]
)
evicted = []
for chat_id, last_used in sorted_by_time[:evict_count]:
if chat_id in self.learners:
# 先保存再淘汰
self.learners[chat_id].save(self.model_save_path)
del self.learners[chat_id]
del self.learner_last_used[chat_id]
evicted.append(chat_id)
if evicted:
logger.info(f"StyleLearner LRU淘汰: 释放了 {len(evicted)} 个不活跃的学习器")
def get_learner(self, chat_id: str, model_config: dict | None = None) -> StyleLearner:
"""
获取或创建指定chat_id的学习器
@@ -463,7 +494,13 @@ class StyleLearnerManager:
Returns:
StyleLearner实例
"""
# 🔧 更新最后使用时间
self.learner_last_used[chat_id] = time.time()
if chat_id not in self.learners:
# 🔧 检查是否需要淘汰
self._evict_if_needed()
# 创建新的学习器
learner = StyleLearner(chat_id, model_config)

View File

@@ -168,15 +168,22 @@ class ImageManager:
image_bytes = base64.b64decode(image_base64)
image_hash = hashlib.md5(image_bytes).hexdigest()
# 如果缓存命中,可以提前释放 image_bytes
# 但如果需要保存表情包,则需要保留 image_bytes
# 2. 优先查询已注册表情的缓存Emoji表
if full_description := await emoji_manager.get_emoji_description_by_hash(image_hash):
logger.info("[缓存命中] 使用已注册表情包(Emoji表)的完整描述")
del image_bytes # 缓存命中,不再需要
del image_base64
refined_part = full_description.split(" Keywords:")[0]
return f"[表情包:{refined_part}]"
# 3. 查询通用图片描述缓存ImageDescriptions表
if cached_description := await self._get_description_from_db(image_hash, "emoji"):
logger.info("[缓存命中] 使用通用图片缓存(ImageDescriptions表)中的描述")
del image_bytes # 缓存命中,不再需要
del image_base64
refined_part = cached_description.split(" Keywords:")[0]
return f"[表情包:{refined_part}]"
@@ -209,7 +216,11 @@ class ImageManager:
await self._save_description_to_db(image_hash, full_description, "emoji")
logger.info(f"新生成的表情包描述已存入通用缓存 (Hash: {image_hash[:8]}...)")
# 6. 返回新生成的描述中用于显示的“精炼描述”部分
# 内存优化:处理完成后主动释放大型二进制数据
del image_bytes
del image_base64
# 6. 返回新生成的描述中用于显示的"精炼描述"部分
refined_part = full_description.split(" Keywords:")[0]
return f"[表情包:{refined_part}]"
@@ -248,11 +259,17 @@ class ImageManager:
existing_image = result.scalar()
if existing_image and existing_image.description:
logger.debug(f"[缓存命中] 使用Images表中的图片描述: {existing_image.description[:50]}...")
# 缓存命中,释放 base64 和 image_bytes
del image_bytes
del image_base64
return f"[图片:{existing_image.description}]"
# 3. 其次查询 ImageDescriptions 表缓存
if cached_description := await self._get_description_from_db(image_hash, "image"):
logger.debug(f"[缓存命中] 使用ImageDescriptions表中的描述: {cached_description[:50]}...")
# 缓存命中,释放 base64 和 image_bytes
del image_bytes
del image_base64
return f"[图片:{cached_description}]"
# 4. 如果都未命中则同步调用VLM生成新描述
@@ -301,6 +318,10 @@ class ImageManager:
logger.info(f"新生成的图片描述已存入缓存 (Hash: {image_hash[:8]}...)")
# 内存优化:处理完成后主动释放大型二进制数据
del image_bytes
del image_base64
return f"[图片:{description}]"
except Exception as e:

View File

@@ -387,8 +387,36 @@ class StreamToolHistoryManager:
return result
# 全局管理器字典按chat_id索引
# 内存优化:全局管理器字典按chat_id索引,添加 LRU 淘汰
_stream_managers: dict[str, StreamToolHistoryManager] = {}
_stream_managers_last_used: dict[str, float] = {} # 记录最后使用时间
_STREAM_MANAGERS_MAX_SIZE = 100 # 最大保留数量
def _evict_old_stream_managers() -> None:
"""内存优化:淘汰最久未使用的 stream manager"""
import time
if len(_stream_managers) < _STREAM_MANAGERS_MAX_SIZE:
return
# 按最后使用时间排序,淘汰最旧的 20%
evict_count = max(1, len(_stream_managers) // 5)
sorted_by_time = sorted(
_stream_managers_last_used.items(),
key=lambda x: x[1]
)
evicted = []
for chat_id, _ in sorted_by_time[:evict_count]:
if chat_id in _stream_managers:
del _stream_managers[chat_id]
if chat_id in _stream_managers_last_used:
del _stream_managers_last_used[chat_id]
evicted.append(chat_id)
if evicted:
logger.info(f"🔧 StreamToolHistoryManager LRU淘汰: 释放了 {len(evicted)} 个不活跃的管理器")
def get_stream_tool_history_manager(chat_id: str) -> StreamToolHistoryManager:
@@ -400,7 +428,14 @@ def get_stream_tool_history_manager(chat_id: str) -> StreamToolHistoryManager:
Returns:
工具历史记录管理器实例
"""
import time
# 🔧 更新最后使用时间
_stream_managers_last_used[chat_id] = time.time()
if chat_id not in _stream_managers:
# 🔧 检查是否需要淘汰
_evict_old_stream_managers()
_stream_managers[chat_id] = StreamToolHistoryManager(chat_id)
return _stream_managers[chat_id]
@@ -413,4 +448,6 @@ def cleanup_stream_manager(chat_id: str) -> None:
"""
if chat_id in _stream_managers:
del _stream_managers[chat_id]
logger.info(f"已清理聊天 {chat_id} 的工具历史记录管理器")
if chat_id in _stream_managers_last_used:
del _stream_managers_last_used[chat_id]
logger.info(f"已清理聊天 {chat_id} 的工具历史记录管理器")

View File

@@ -14,11 +14,19 @@ logger = get_logger("relationship_service")
class RelationshipService:
"""用户关系分服务 - 独立于插件的数据库直接访问层"""
"""用户关系分服务 - 独立于插件的数据库直接访问层
内存优化:添加缓存大小限制和自动过期清理
"""
# 🔧 缓存配置
CACHE_MAX_SIZE = 1000 # 最大缓存用户数
def __init__(self):
self._cache: dict[str, dict] = {} # user_id -> {score, text, last_updated}
self._cache_ttl = 300 # 缓存5分钟
self._last_cleanup = time.time() # 上次清理时间
self._cleanup_interval = 60 # 每60秒清理一次过期条目
async def get_user_relationship_score(self, user_id: str) -> float:
"""
@@ -162,6 +170,9 @@ class RelationshipService:
def _get_from_cache(self, user_id: str) -> dict | None:
"""从缓存获取数据"""
# 🔧 触发定期清理
self._maybe_cleanup_expired()
if user_id in self._cache:
cached_data = self._cache[user_id]
if time.time() - cached_data["last_updated"] < self._cache_ttl:
@@ -173,12 +184,46 @@ class RelationshipService:
def _update_cache(self, user_id: str, score: float, text: str):
"""更新缓存"""
# 🔧 内存优化:检查缓存大小限制
if len(self._cache) >= self.CACHE_MAX_SIZE and user_id not in self._cache:
# 淘汰最旧的 10% 条目
self._evict_oldest_entries()
self._cache[user_id] = {
"score": score,
"text": text,
"last_updated": time.time()
}
def _maybe_cleanup_expired(self):
"""🔧 内存优化:定期清理过期条目"""
now = time.time()
if now - self._last_cleanup < self._cleanup_interval:
return
self._last_cleanup = now
expired_keys = []
for user_id, data in self._cache.items():
if now - data["last_updated"] >= self._cache_ttl:
expired_keys.append(user_id)
for key in expired_keys:
del self._cache[key]
if expired_keys:
logger.debug(f"🔧 relationship_service 清理了 {len(expired_keys)} 个过期缓存条目")
def _evict_oldest_entries(self):
"""🔧 内存优化:淘汰最旧的条目"""
evict_count = max(1, len(self._cache) // 10)
sorted_entries = sorted(
self._cache.items(),
key=lambda x: x[1]["last_updated"]
)
for user_id, _ in sorted_entries[:evict_count]:
del self._cache[user_id]
logger.debug(f"🔧 relationship_service LRU淘汰了 {evict_count} 个缓存条目")
async def _fetch_from_database(self, user_id: str) -> UserRelationships | None:
"""从数据库获取关系数据"""
try: