mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-09-30 09:10:01 +08:00
fix: add asyncio.Lock to prevent concurrent snapshot building race condition
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
## 项目概述
|
||||
|
||||
优化 `zhenxun_bot` 的权限检查系统,将每条消息的数据库/缓存查询次数从 **6-10次** 降低到 **1-2次**。
|
||||
优化 `zhenxun_bot` 的权限检查系统,将每条消息的数据库/缓存查询次数从 **6-10 次** 降低到 **1-2 次**。
|
||||
|
||||
---
|
||||
|
||||
@@ -12,15 +12,15 @@
|
||||
|
||||
每条消息进入时,权限检查系统执行以下查询:
|
||||
|
||||
| 阶段 | 查询内容 | 次数 |
|
||||
|-----|---------|------|
|
||||
| `_load_context` | PluginInfo, UserConsole, GroupConsole | 3次 |
|
||||
| `auth_ban` | BanConsole | 1-2次 |
|
||||
| `auth_bot` | BotConsole | 1次 |
|
||||
| `auth_admin` | LevelUser (全局+群组) | 1-2次 |
|
||||
| `auth_limit` | PluginLimit (如果不在内存) | 0-1次 |
|
||||
| 阶段 | 查询内容 | 次数 |
|
||||
| --------------- | ------------------------------------- | ------ |
|
||||
| `_load_context` | PluginInfo, UserConsole, GroupConsole | 3 次 |
|
||||
| `auth_ban` | BanConsole | 1-2 次 |
|
||||
| `auth_bot` | BotConsole | 1 次 |
|
||||
| `auth_admin` | LevelUser (全局+群组) | 1-2 次 |
|
||||
| `auth_limit` | PluginLimit (如果不在内存) | 0-1 次 |
|
||||
|
||||
**总计:6-10次查询**
|
||||
**总计:6-10 次查询**
|
||||
|
||||
### 问题根源
|
||||
|
||||
@@ -49,11 +49,11 @@ Hash 结构:
|
||||
"user_gold": 100, # 用户金币
|
||||
"user_banned": 0, # 0=未ban, -1=永久ban, >0=ban结束时间戳
|
||||
"user_ban_duration": 0, # ban时长(用于计算剩余时间)
|
||||
|
||||
|
||||
# === 用户权限等级 ===
|
||||
"user_level_global": 0, # 全局权限等级
|
||||
"user_level_group": 0, # 群组权限等级
|
||||
|
||||
|
||||
# === 群组信息 ===
|
||||
"group_status": 1, # 群组状态 (1=开启, 0=休眠)
|
||||
"group_level": 5, # 群组等级
|
||||
@@ -61,11 +61,11 @@ Hash 结构:
|
||||
"group_block_plugins": "", # 禁用插件列表 "<plugin1,<plugin2,"
|
||||
"group_superuser_block_plugins": "", # 超级用户禁用插件列表
|
||||
"group_banned": 0, # 群组是否被ban
|
||||
|
||||
|
||||
# === Bot信息 ===
|
||||
"bot_status": 1, # Bot状态
|
||||
"bot_block_plugins": "", # Bot禁用插件列表
|
||||
|
||||
|
||||
# === 元数据 ===
|
||||
"version": 1, # 快照版本(用于失效判断)
|
||||
"created_at": 1703859600 # 创建时间戳
|
||||
@@ -136,20 +136,20 @@ Hash 结构:
|
||||
|
||||
#### 主动失效(事件驱动)
|
||||
|
||||
| 事件 | 失效范围 |
|
||||
|-----|---------|
|
||||
| 用户金币变化 | `AUTH_SNAPSHOT:{user_id}:*:*` |
|
||||
| 用户被 ban/unban | `AUTH_SNAPSHOT:{user_id}:*:*` |
|
||||
| 群组设置变更 | `AUTH_SNAPSHOT:*:{group_id}:*` |
|
||||
| Bot 配置变更 | `AUTH_SNAPSHOT:*:*:{bot_id}` |
|
||||
| 插件配置变更 | `PLUGIN_SNAPSHOT:{module}` + 本地内存缓存 |
|
||||
| 用户权限变更 | `AUTH_SNAPSHOT:{user_id}:{group_id}:*` |
|
||||
| 事件 | 失效范围 |
|
||||
| ---------------- | ----------------------------------------- |
|
||||
| 用户金币变化 | `AUTH_SNAPSHOT:{user_id}:*:*` |
|
||||
| 用户被 ban/unban | `AUTH_SNAPSHOT:{user_id}:*:*` |
|
||||
| 群组设置变更 | `AUTH_SNAPSHOT:*:{group_id}:*` |
|
||||
| Bot 配置变更 | `AUTH_SNAPSHOT:*:*:{bot_id}` |
|
||||
| 插件配置变更 | `PLUGIN_SNAPSHOT:{module}` + 本地内存缓存 |
|
||||
| 用户权限变更 | `AUTH_SNAPSHOT:{user_id}:{group_id}:*` |
|
||||
|
||||
#### 被动失效(TTL)
|
||||
|
||||
- 权限快照 TTL:**60秒**(权衡实时性和性能)
|
||||
- 插件快照 TTL:**300秒**(插件配置变化较少)
|
||||
- 本地内存缓存 TTL:**30秒**
|
||||
- 权限快照 TTL:**60 秒**(权衡实时性和性能)
|
||||
- 插件快照 TTL:**300 秒**(插件配置变化较少)
|
||||
- 本地内存缓存 TTL:**30 秒**
|
||||
|
||||
---
|
||||
|
||||
@@ -165,11 +165,12 @@ Hash 结构:
|
||||
### Phase 2: 快照服务 ✅ [已完成]
|
||||
|
||||
- [x] 创建 `AuthSnapshotService` 类
|
||||
|
||||
- [x] `get_snapshot(user_id, group_id, bot_id)` - 获取权限快照
|
||||
- [x] `build_snapshot(user_id, group_id, bot_id)` - 构建权限快照
|
||||
- [x] `invalidate_user(user_id)` - 失效用户相关快照
|
||||
- [x] `invalidate_group(group_id)` - 失效群组相关快照
|
||||
- [x] `invalidate_bot(bot_id)` - 失效Bot相关快照
|
||||
- [x] `invalidate_bot(bot_id)` - 失效 Bot 相关快照
|
||||
|
||||
- [x] 创建 `PluginSnapshotService` 类
|
||||
- [x] `get_plugin(module)` - 获取插件信息(本地缓存优先)
|
||||
@@ -222,22 +223,22 @@ zhenxun/
|
||||
|
||||
## 性能预期
|
||||
|
||||
| 指标 | 优化前 | 优化后 | 提升 |
|
||||
|-----|-------|-------|-----|
|
||||
| 查询次数 | 6-10次 | 1-2次 | 80%↓ |
|
||||
| 平均延迟 | ~50ms | ~10ms | 80%↓ |
|
||||
| Redis 连接压力 | 高 | 低 | 显著降低 |
|
||||
| 指标 | 优化前 | 优化后 | 提升 |
|
||||
| -------------- | ------- | ------ | -------- |
|
||||
| 查询次数 | 6-10 次 | 1-2 次 | 80%↓ |
|
||||
| 平均延迟 | ~50ms | ~10ms | 80%↓ |
|
||||
| Redis 连接压力 | 高 | 低 | 显著降低 |
|
||||
|
||||
---
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|-----|---------|
|
||||
| 快照数据过期 | 合理的 TTL + 主动失效机制 |
|
||||
| 风险 | 缓解措施 |
|
||||
| ------------ | ------------------------------- |
|
||||
| 快照数据过期 | 合理的 TTL + 主动失效机制 |
|
||||
| 快照构建延迟 | 异步构建 + 首次访问降级到旧流程 |
|
||||
| 内存占用增加 | 监控内存使用 + 合理的缓存清理 |
|
||||
| 数据一致性 | 写操作后立即失效缓存 |
|
||||
| 内存占用增加 | 监控内存使用 + 合理的缓存清理 |
|
||||
| 数据一致性 | 写操作后立即失效缓存 |
|
||||
|
||||
---
|
||||
|
||||
@@ -304,4 +305,3 @@ await PluginSnapshotService.invalidate_plugin("example_plugin")
|
||||
- 开始日期:2025-12-29
|
||||
- 当前阶段:核心功能已完成
|
||||
- 状态:✅ 基础功能完成,待测试验证
|
||||
|
||||
|
||||
@@ -37,6 +37,9 @@ class AuthSnapshotService:
|
||||
# 正在构建中的快照(防止并发重复构建)
|
||||
_building: ClassVar[dict[str, asyncio.Future]] = {}
|
||||
|
||||
# 构建锁(按 cache_key 粒度)
|
||||
_build_locks: ClassVar[dict[str, asyncio.Lock]] = {}
|
||||
|
||||
@classmethod
|
||||
def _build_cache_key(cls, user_id: str, group_id: str | None, bot_id: str) -> str:
|
||||
"""构建缓存键"""
|
||||
@@ -84,15 +87,26 @@ class AuthSnapshotService:
|
||||
except Exception as e:
|
||||
logger.debug(f"从Redis获取权限快照失败: {cache_key}", LOG_COMMAND, e=e)
|
||||
|
||||
# 3. 检查是否正在构建中(防止并发)
|
||||
if cache_key in cls._building:
|
||||
try:
|
||||
return await cls._building[cache_key]
|
||||
except Exception:
|
||||
pass
|
||||
# 3. 获取或创建该 cache_key 的锁
|
||||
if cache_key not in cls._build_locks:
|
||||
cls._build_locks[cache_key] = asyncio.Lock()
|
||||
lock = cls._build_locks[cache_key]
|
||||
|
||||
# 4. 构建新快照
|
||||
return await cls._build_and_cache(user_id, group_id, bot_id, cache_key)
|
||||
# 4. 使用锁保护构建过程,防止并发重复构建
|
||||
async with lock:
|
||||
# 再次检查缓存(可能在等待锁的过程中已被其他协程构建)
|
||||
if snapshot := cls._get_from_memory(cache_key):
|
||||
return snapshot
|
||||
|
||||
# 检查是否正在构建中(其他协程已开始构建)
|
||||
if cache_key in cls._building:
|
||||
try:
|
||||
return await cls._building[cache_key]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 5. 构建新快照
|
||||
return await cls._build_and_cache(user_id, group_id, bot_id, cache_key)
|
||||
|
||||
@classmethod
|
||||
def _get_from_memory(cls, cache_key: str) -> AuthSnapshot | None:
|
||||
@@ -222,8 +236,20 @@ class AuthSnapshotService:
|
||||
def clear_all_cache(cls):
|
||||
"""清空所有缓存"""
|
||||
cls._memory_cache.clear()
|
||||
cls._build_locks.clear()
|
||||
logger.info("已清空所有权限快照缓存", LOG_COMMAND)
|
||||
|
||||
@classmethod
|
||||
def cleanup_locks(cls):
|
||||
"""清理未被使用的锁(可定期调用)"""
|
||||
# 只保留正在使用的锁
|
||||
active_keys = set(cls._building.keys())
|
||||
keys_to_remove = [k for k in cls._build_locks if k not in active_keys]
|
||||
for key in keys_to_remove:
|
||||
lock = cls._build_locks.get(key)
|
||||
if lock and not lock.locked():
|
||||
del cls._build_locks[key]
|
||||
|
||||
|
||||
class PluginSnapshotService:
|
||||
"""插件快照服务
|
||||
@@ -239,6 +265,9 @@ class PluginSnapshotService:
|
||||
# 正在构建中的快照
|
||||
_building: ClassVar[dict[str, asyncio.Future]] = {}
|
||||
|
||||
# 构建锁(按 cache_key 粒度)
|
||||
_build_locks: ClassVar[dict[str, asyncio.Lock]] = {}
|
||||
|
||||
@classmethod
|
||||
def _build_cache_key(cls, module: str) -> str:
|
||||
"""构建缓存键"""
|
||||
@@ -276,15 +305,26 @@ class PluginSnapshotService:
|
||||
except Exception as e:
|
||||
logger.debug(f"从Redis获取插件快照失败: {module}", LOG_COMMAND, e=e)
|
||||
|
||||
# 3. 检查是否正在构建中
|
||||
if cache_key in cls._building:
|
||||
try:
|
||||
return await cls._building[cache_key]
|
||||
except Exception:
|
||||
pass
|
||||
# 3. 获取或创建该 cache_key 的锁
|
||||
if cache_key not in cls._build_locks:
|
||||
cls._build_locks[cache_key] = asyncio.Lock()
|
||||
lock = cls._build_locks[cache_key]
|
||||
|
||||
# 4. 从数据库构建
|
||||
return await cls._build_and_cache(module, cache_key)
|
||||
# 4. 使用锁保护构建过程
|
||||
async with lock:
|
||||
# 再次检查缓存(可能在等待锁的过程中已被其他协程构建)
|
||||
if snapshot := cls._get_from_memory(cache_key):
|
||||
return snapshot
|
||||
|
||||
# 检查是否正在构建中
|
||||
if cache_key in cls._building:
|
||||
try:
|
||||
return await cls._building[cache_key]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 5. 从数据库构建
|
||||
return await cls._build_and_cache(module, cache_key)
|
||||
|
||||
@classmethod
|
||||
def _get_from_memory(cls, cache_key: str) -> PluginSnapshot | None:
|
||||
@@ -406,4 +446,15 @@ class PluginSnapshotService:
|
||||
def clear_all_cache(cls):
|
||||
"""清空所有缓存"""
|
||||
cls._memory_cache.clear()
|
||||
cls._build_locks.clear()
|
||||
logger.info("已清空所有插件快照缓存", LOG_COMMAND)
|
||||
|
||||
@classmethod
|
||||
def cleanup_locks(cls):
|
||||
"""清理未被使用的锁(可定期调用)"""
|
||||
active_keys = set(cls._building.keys())
|
||||
keys_to_remove = [k for k in cls._build_locks if k not in active_keys]
|
||||
for key in keys_to_remove:
|
||||
lock = cls._build_locks.get(key)
|
||||
if lock and not lock.locked():
|
||||
del cls._build_locks[key]
|
||||
|
||||
Reference in New Issue
Block a user