fix: add asyncio.Lock to prevent concurrent snapshot building race condition

This commit is contained in:
HibiKier
2025-12-29 09:29:32 +08:00
parent 96ba8d5a21
commit ea8d874f0c
2 changed files with 103 additions and 52 deletions
+36 -36
View File
@@ -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
- 当前阶段:核心功能已完成
- 状态:✅ 基础功能完成,待测试验证
+67 -16
View File
@@ -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]