From 99785d09ad9d63ed62f0967ccc679a6bfde487be Mon Sep 17 00:00:00 2001 From: Windpicker-owo <3431391539@qq.com> Date: Sun, 2 Nov 2025 13:25:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(cache):=20=E6=B7=BB=E5=8A=A0=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89TTL=E6=94=AF=E6=8C=81=E5=92=8C=E5=86=85?= =?UTF-8?q?=E5=AD=98=E9=99=90=E5=88=B6=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/optimization/cache_manager.py | 151 ++++++++++++++++-- src/common/database/utils/decorators.py | 9 +- src/config/official_configs.py | 11 +- template/bot_config_template.toml | 11 +- 4 files changed, 162 insertions(+), 20 deletions(-) diff --git a/src/common/database/optimization/cache_manager.py b/src/common/database/optimization/cache_manager.py index 240885b3a..7b95cca97 100644 --- a/src/common/database/optimization/cache_manager.py +++ b/src/common/database/optimization/cache_manager.py @@ -137,6 +137,7 @@ class LRUCache(Generic[T]): key: str, value: T, size: int | None = None, + ttl: float | None = None, ) -> None: """设置缓存值 @@ -144,6 +145,7 @@ class LRUCache(Generic[T]): key: 缓存键 value: 缓存值 size: 数据大小(字节),如果为None则尝试估算 + ttl: 自定义过期时间(秒),如果为None则使用默认TTL """ async with self._lock: now = time.time() @@ -157,10 +159,21 @@ class LRUCache(Generic[T]): if size is None: size = self._estimate_size(value) - # 创建新条目 + # 创建新条目(如果指定了ttl,则修改created_at来实现自定义TTL) + # 通过调整created_at,使得: now - created_at + custom_ttl = self.ttl + # 即: created_at = now - (self.ttl - custom_ttl) + if ttl is not None and ttl != self.ttl: + # 调整创建时间以实现自定义TTL + adjusted_created_at = now - (self.ttl - ttl) + logger.debug( + f"[{self.name}] 使用自定义TTL {ttl}s (默认{self.ttl}s) for key: {key}" + ) + else: + adjusted_created_at = now + entry = CacheEntry( value=value, - created_at=now, + created_at=adjusted_created_at, last_accessed=now, access_count=0, size=size, @@ -245,6 +258,7 @@ class MultiLevelCache: l1_ttl: float = 60, l2_max_size: int = 10000, l2_ttl: float = 300, + max_memory_mb: int = 100, ): """初始化多级缓存 @@ -253,14 +267,16 @@ class MultiLevelCache: l1_ttl: L1缓存TTL(秒) l2_max_size: L2缓存最大条目数 l2_ttl: L2缓存TTL(秒) + max_memory_mb: 最大内存占用(MB) """ self.l1_cache: LRUCache[Any] = LRUCache(l1_max_size, l1_ttl, "L1") self.l2_cache: LRUCache[Any] = LRUCache(l2_max_size, l2_ttl, "L2") + self.max_memory_bytes = max_memory_mb * 1024 * 1024 self._cleanup_task: asyncio.Task | None = None logger.info( f"多级缓存初始化: L1({l1_max_size}项/{l1_ttl}s) " - f"L2({l2_max_size}项/{l2_ttl}s)" + f"L2({l2_max_size}项/{l2_ttl}s) 内存上限({max_memory_mb}MB)" ) async def get( @@ -309,6 +325,7 @@ class MultiLevelCache: key: str, value: Any, size: int | None = None, + ttl: float | None = None, ) -> None: """设置缓存值 @@ -318,9 +335,25 @@ class MultiLevelCache: key: 缓存键 value: 缓存值 size: 数据大小(字节) + ttl: 自定义过期时间(秒),如果为None则使用默认TTL """ - await self.l1_cache.set(key, value, size) - await self.l2_cache.set(key, value, size) + # 根据TTL决定写入哪个缓存层 + if ttl is not None: + # 有自定义TTL,根据TTL大小决定写入层级 + if ttl <= self.l1_cache.ttl: + # 短TTL,只写入L1 + await self.l1_cache.set(key, value, size, ttl) + elif ttl <= self.l2_cache.ttl: + # 中等TTL,写入L1和L2 + await self.l1_cache.set(key, value, size, ttl) + await self.l2_cache.set(key, value, size, ttl) + else: + # 长TTL,只写入L2 + await self.l2_cache.set(key, value, size, ttl) + else: + # 没有自定义TTL,使用默认行为(同时写入L1和L2) + await self.l1_cache.set(key, value, size) + await self.l2_cache.set(key, value, size) async def delete(self, key: str) -> None: """删除缓存条目 @@ -339,13 +372,44 @@ class MultiLevelCache: await self.l2_cache.clear() logger.info("所有缓存已清空") - async def get_stats(self) -> dict[str, CacheStats]: + async def get_stats(self) -> dict[str, Any]: """获取所有缓存层的统计信息""" + l1_stats = await self.l1_cache.get_stats() + l2_stats = await self.l2_cache.get_stats() + total_size_bytes = l1_stats.total_size + l2_stats.total_size + return { - "l1": await self.l1_cache.get_stats(), - "l2": await self.l2_cache.get_stats(), + "l1": l1_stats, + "l2": l2_stats, + "total_memory_mb": total_size_bytes / (1024 * 1024), + "max_memory_mb": self.max_memory_bytes / (1024 * 1024), + "memory_usage_percent": (total_size_bytes / self.max_memory_bytes * 100) if self.max_memory_bytes > 0 else 0, } + async def check_memory_limit(self) -> None: + """检查并强制清理超出内存限制的缓存""" + stats = await self.get_stats() + total_size = stats["l1"].total_size + stats["l2"].total_size + + if total_size > self.max_memory_bytes: + memory_mb = total_size / (1024 * 1024) + max_mb = self.max_memory_bytes / (1024 * 1024) + logger.warning( + f"缓存内存超限: {memory_mb:.2f}MB / {max_mb:.2f}MB " + f"({stats['memory_usage_percent']:.1f}%),开始强制清理L2缓存" + ) + # 优先清理L2缓存(温数据) + await self.l2_cache.clear() + + # 如果清理L2后仍超限,清理L1 + stats_after_l2 = await self.get_stats() + total_after_l2 = stats_after_l2["l1"].total_size + stats_after_l2["l2"].total_size + if total_after_l2 > self.max_memory_bytes: + logger.warning("清理L2后仍超限,继续清理L1缓存") + await self.l1_cache.clear() + + logger.info("缓存强制清理完成") + async def start_cleanup_task(self, interval: float = 60) -> None: """启动定期清理任务 @@ -361,12 +425,20 @@ class MultiLevelCache: try: await asyncio.sleep(interval) stats = await self.get_stats() + l1_stats = stats["l1"] + l2_stats = stats["l2"] logger.info( - f"缓存统计 - L1: {stats['l1'].item_count}项, " - f"命中率{stats['l1'].hit_rate:.2%} | " - f"L2: {stats['l2'].item_count}项, " - f"命中率{stats['l2'].hit_rate:.2%}" + f"缓存统计 - L1: {l1_stats.item_count}项, " + f"命中率{l1_stats.hit_rate:.2%} | " + f"L2: {l2_stats.item_count}项, " + f"命中率{l2_stats.hit_rate:.2%} | " + f"内存: {stats['total_memory_mb']:.2f}MB/{stats['max_memory_mb']:.2f}MB " + f"({stats['memory_usage_percent']:.1f}%)" ) + + # 检查内存限制 + await self.check_memory_limit() + except asyncio.CancelledError: break except Exception as e: @@ -393,14 +465,63 @@ _cache_lock = asyncio.Lock() async def get_cache() -> MultiLevelCache: - """获取全局缓存实例(单例)""" + """获取全局缓存实例(单例) + + 从配置文件读取缓存参数,如果配置未加载则使用默认值 + 如果配置中禁用了缓存,返回一个最小化的缓存实例(容量为1) + """ global _global_cache if _global_cache is None: async with _cache_lock: if _global_cache is None: - _global_cache = MultiLevelCache() - await _global_cache.start_cleanup_task() + # 尝试从配置读取参数 + try: + from src.config.config import global_config + + db_config = global_config.database + + # 检查是否启用缓存 + if not db_config.enable_database_cache: + logger.info("数据库缓存已禁用,使用最小化缓存实例") + _global_cache = MultiLevelCache( + l1_max_size=1, + l1_ttl=1, + l2_max_size=1, + l2_ttl=1, + max_memory_mb=1, + ) + return _global_cache + + l1_max_size = db_config.cache_l1_max_size + l1_ttl = db_config.cache_l1_ttl + l2_max_size = db_config.cache_l2_max_size + l2_ttl = db_config.cache_l2_ttl + max_memory_mb = db_config.cache_max_memory_mb + cleanup_interval = db_config.cache_cleanup_interval + + logger.info( + f"从配置加载缓存参数: L1({l1_max_size}/{l1_ttl}s), " + f"L2({l2_max_size}/{l2_ttl}s), 内存限制({max_memory_mb}MB)" + ) + except Exception as e: + # 配置未加载,使用默认值 + logger.warning(f"无法从配置加载缓存参数,使用默认值: {e}") + l1_max_size = 1000 + l1_ttl = 60 + l2_max_size = 10000 + l2_ttl = 300 + max_memory_mb = 100 + cleanup_interval = 60 + + _global_cache = MultiLevelCache( + l1_max_size=l1_max_size, + l1_ttl=l1_ttl, + l2_max_size=l2_max_size, + l2_ttl=l2_ttl, + max_memory_mb=max_memory_mb, + ) + await _global_cache.start_cleanup_task(interval=cleanup_interval) return _global_cache diff --git a/src/common/database/utils/decorators.py b/src/common/database/utils/decorators.py index a5c4fdc43..c2035cd82 100644 --- a/src/common/database/utils/decorators.py +++ b/src/common/database/utils/decorators.py @@ -198,9 +198,12 @@ def cached( # 执行函数 result = await func(*args, **kwargs) - # 写入缓存(注意:MultiLevelCache.set不支持ttl参数,使用L1缓存的默认TTL) - await cache.set(cache_key, result) - logger.debug(f"缓存写入: {cache_key}") + # 写入缓存,传递自定义TTL参数 + await cache.set(cache_key, result, ttl=ttl) + if ttl is not None: + logger.debug(f"缓存写入: {cache_key} (TTL={ttl}s)") + else: + logger.debug(f"缓存写入: {cache_key} (使用默认TTL)") return result diff --git a/src/config/official_configs.py b/src/config/official_configs.py index cc0799a83..2dbf5d252 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -30,7 +30,7 @@ class DatabaseConfig(ValidatedConfigBase): ) mysql_ssl_ca: str = Field(default="", description="SSL CA证书路径") mysql_ssl_cert: str = Field(default="", description="SSL客户端证书路径") - mysql_ssl_key: str = Field(default="", description="SSL客户端密钥路径") + mysql_ssl_key: str = Field(default="", description="SSL密钥路径") mysql_autocommit: bool = Field(default=True, description="自动提交事务") mysql_sql_mode: str = Field(default="TRADITIONAL", description="SQL模式") connection_pool_size: int = Field(default=10, ge=1, description="连接池大小") @@ -41,6 +41,15 @@ class DatabaseConfig(ValidatedConfigBase): default=True, description="是否启用批量保存动作记录(开启后将多个动作一次性写入数据库,提升性能)" ) + # 数据库缓存配置 + enable_database_cache: bool = Field(default=True, description="是否启用数据库查询缓存系统") + cache_l1_max_size: int = Field(default=1000, ge=100, le=50000, description="L1缓存最大条目数(热数据,内存占用约1-5MB)") + cache_l1_ttl: int = Field(default=60, ge=10, le=3600, description="L1缓存生存时间(秒)") + cache_l2_max_size: int = Field(default=10000, ge=1000, le=100000, description="L2缓存最大条目数(温数据,内存占用约10-50MB)") + cache_l2_ttl: int = Field(default=300, ge=60, le=7200, description="L2缓存生存时间(秒)") + cache_cleanup_interval: int = Field(default=60, ge=30, le=600, description="缓存清理任务执行间隔(秒)") + cache_max_memory_mb: int = Field(default=100, ge=10, le=1000, description="缓存最大内存占用(MB),超过此值将触发强制清理") + class BotConfig(ValidatedConfigBase): """QQ机器人配置类""" diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index 1b195d588..112d07786 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -1,5 +1,5 @@ [inner] -version = "7.5.4" +version = "7.5.5" #----以下是给开发人员阅读的,如果你只是部署了MoFox-Bot,不需要阅读---- #如果你想要修改配置文件,请递增version的值 @@ -43,6 +43,15 @@ connection_timeout = 10 # 连接超时时间(秒) # 批量动作记录存储配置 batch_action_storage_enabled = true # 是否启用批量保存动作记录(开启后将多个动作一次性写入数据库,提升性能) +# 数据库缓存配置(防止内存溢出) +enable_database_cache = true # 是否启用数据库查询缓存系统 +cache_l1_max_size = 1000 # L1缓存最大条目数(热数据,内存占用约1-5MB) +cache_l1_ttl = 60 # L1缓存生存时间(秒) +cache_l2_max_size = 10000 # L2缓存最大条目数(温数据,内存占用约10-50MB) +cache_l2_ttl = 300 # L2缓存生存时间(秒) +cache_cleanup_interval = 60 # 缓存清理任务执行间隔(秒) +cache_max_memory_mb = 100 # 缓存最大内存占用(MB),超过此值将触发强制清理 + [permission] # 权限系统配置 # Master用户配置(拥有最高权限,无视所有权限节点) # 格式:[[platform, user_id], ...]