Files
zhenxun_bot/zhenxun/builtin_plugins/hooks/auth/IMMEDIATE_IMPROVEMENTS.md
T

12 KiB
Raw Blame History

权限检查系统 - 立即可以实施的改进

当前问题总结

经过分析,当前权限检查系统存在以下主要问题:

1. 检查执行顺序不合理

  • 问题:所有检查并行执行,ban 检查应该优先执行
  • 影响:用户被 ban 后,其他检查(金币、限制等)仍然会执行,浪费资源
  • 优先级:高

2. 缺乏早期退出机制

  • 问题:即使某个检查失败,其他检查仍会继续执行
  • 影响:浪费 CPU 和数据库资源
  • 优先级:高

3. 数据获取可以优化

  • 问题:group 数据在 hooks 执行时才获取,可以提前获取
  • 影响:增加总体执行时间
  • 优先级:中

4. 充分利用 DataAccess 的 Redis 缓存

  • 现状:DataAccess 已经实现了完整的 Redis 缓存机制(包括空结果缓存 5 分钟)
  • 问题:没有充分利用缓存,数据获取顺序不够优化
  • 影响:缓存命中率可能不够高,重复查询数据库
  • 优先级:高

立即可以实施的改进

改进 1:调整检查顺序,ban 检查优先执行

位置:auth_checker.py 第 498-514 行

当前代码:

hook_tasks = [
    time_hook(auth_ban(...), "auth_ban", hook_times),
    time_hook(auth_bot(...), "auth_bot", hook_times),
    # ... 其他检查
]
await asyncio.gather(*hook_tasks)  # 并行执行

改进方案:

# 1. 先执行关键检查(ban、bot),如果失败立即退出
critical_checks = [
    ("auth_ban", lambda: time_hook(auth_ban(...), "auth_ban", hook_times)),
    ("auth_bot", lambda: time_hook(auth_bot(...), "auth_bot", hook_times)),
]

for check_name, check_func in critical_checks:
    try:
        await check_func()
    except SkipPluginException:
        raise  # 立即退出
    except Exception as e:
        logger.error(f"{check_name} 检查失败: {e}", LOGGER_COMMAND, session=session)
        # 关键检查失败,可以选择继续或退出

# 2. 关键检查通过后,再并行执行其他检查
other_checks = [
    time_hook(auth_group(...), "auth_group", hook_times),
    time_hook(auth_admin(...), "auth_admin", hook_times),
    time_hook(auth_plugin(...), "auth_plugin", hook_times),
    time_hook(auth_limit(...), "auth_limit", hook_times),
]
await asyncio.gather(*other_checks)

改进 2:提前获取 group 数据

位置:auth_checker.py 第 485-493 行

当前代码:

# 在hooks执行时才获取group
group = None
if entity.group_id:
    group_dao = DataAccess(GroupConsole)
    group = await with_timeout(...)

改进方案:

# 在获取plugin和user时,同时获取group
# 在 get_plugin_and_user 函数中或auth函数开始处
group = None
if entity.group_id:
    group_dao = DataAccess(GroupConsole)
    group_task = group_dao.safe_get_or_none(
        group_id=entity.group_id, channel_id__isnull=True
    )
    # 可以并行获取
    plugin, user, group = await asyncio.gather(
        plugin_task, user_task, group_task
    )

改进 3:充分利用 DataAccess 的 Redis 缓存机制

重要说明:DataAccess 已经实现了完整的 Redis 缓存机制,包括:

  • ✅ 自动缓存查询结果
  • ✅ 空结果缓存(5 分钟 TTL,避免频繁查询不存在的记录)
  • ✅ 缓存统计(命中率、命中次数等)
  • ✅ 自动处理缓存的获取和设置

优化策略:

3.1 优化数据获取顺序,提高缓存命中率

位置:auth_checker.py 的 get_plugin_and_user 函数和 auth 函数

当前问题:

  • plugin、user、group 数据分别获取,可能错过并行获取的缓存优势
  • group 数据在 hooks 执行时才获取,应该提前获取

改进方案:

async def get_plugin_user_and_group(
    module: str, user_id: str, group_id: str | None
) -> tuple[PluginInfo, UserConsole, GroupConsole | None]:
    """统一获取插件、用户和群组数据(充分利用 DataAccess 缓存)

    注意:DataAccess 会自动使用 Redis 缓存,包括:
    - 查询结果缓存
    - 空结果缓存(5分钟)
    - 缓存统计
    """
    user_dao = DataAccess(UserConsole)
    plugin_dao = DataAccess(PluginInfo)
    group_dao = DataAccess(GroupConsole) if group_id else None

    # 并行获取所有数据,DataAccess 会自动处理缓存
    tasks = [
        plugin_dao.safe_get_or_none(module=module),
        user_dao.get_by_func_or_none(
            UserConsole.get_user, False, user_id=user_id
        )
    ]

    if group_id and group_dao:
        tasks.append(
            group_dao.safe_get_or_none(
                group_id=group_id, channel_id__isnull=True
            )
        )
    else:
        tasks.append(asyncio.create_task(asyncio.sleep(0)))  # 占位

    try:
        results = await with_timeout(
            asyncio.gather(*tasks), name="get_plugin_user_group"
        )
        plugin = results[0]
        user = results[1]
        group = results[2] if len(results) > 2 and group_id else None

        # 验证数据
        if not plugin:
            raise PermissionExemption(f"插件:{module} 数据不存在...")
        if plugin.plugin_type == PluginType.HIDDEN:
            raise PermissionExemption(f"插件: {plugin.name}:{plugin.module} 为HIDDEN...")
        if not user:
            raise PermissionExemption("用户数据不存在...")

        return plugin, user, group
    except asyncio.TimeoutError:
        # 超时时尝试从缓存获取(DataAccess 已经缓存了)
        logger.warning(
            f"数据获取超时,尝试从缓存获取,模块: {module}",
            LOGGER_COMMAND
        )
        # DataAccess 的缓存会自动生效,这里只需要重试一次
        plugin = await plugin_dao.safe_get_or_none(module=module)
        user = await user_dao.get_by_func_or_none(
            UserConsole.get_user, False, user_id=user_id
        )
        group = None
        if group_id and group_dao:
            group = await group_dao.safe_get_or_none(
                group_id=group_id, channel_id__isnull=True
            )

        if not plugin or not user:
            raise PermissionExemption("获取数据失败,请稍后再试...")

        return plugin, user, group

3.2 监控缓存命中率

位置:在权限检查中添加缓存统计监控

实现方案:

from zhenxun.services.data_access import DataAccess

# 在 auth 函数开始和结束时记录缓存统计
async def auth(...):
    # 记录开始时的缓存统计
    cache_stats_before = DataAccess.get_cache_stats()

    try:
        # ... 执行权限检查 ...
        pass
    finally:
        # 记录结束时的缓存统计
        cache_stats_after = DataAccess.get_cache_stats()

        # 计算本次检查的缓存命中情况
        for before, after in zip(cache_stats_before, cache_stats_after):
            hits_diff = after["hits"] - before["hits"]
            misses_diff = after["misses"] - before["misses"]
            total = hits_diff + misses_diff
            if total > 0:
                hit_rate = (hits_diff / total) * 100
                if hit_rate < 50:  # 缓存命中率低于50%时记录警告
                    logger.warning(
                        f"缓存命中率较低: {after['cache_type']} = {hit_rate:.1f}%",
                        LOGGER_COMMAND
                    )

3.3 优化检查函数中的数据访问

说明:各个检查函数(auth_ban、auth_bot 等)中使用的 DataAccess 已经自动缓存,无需额外处理。

建议:

  • 确保所有数据访问都通过 DataAccess,而不是直接使用模型类
  • 对于频繁查询的数据(如 ban 记录),DataAccess 会自动缓存
  • 空结果也会被缓存 5 分钟,避免频繁查询不存在的记录

改进 4:统一错误处理策略

位置:各个检查函数

当前问题:

  • auth_ban 超时时不阻塞
  • auth_bot 超时时不阻塞
  • auth_limit 超时时不阻塞
  • 但 get_plugin_and_user 超时时会抛出异常

改进方案:

# 定义统一的超时处理策略
class TimeoutStrategy:
    FAIL_FAST = "fail_fast"  # 超时立即失败
    CONTINUE = "continue"     # 超时继续执行
    CACHE_FALLBACK = "cache_fallback"  # 超时使用缓存

# 根据检查的重要性设置策略
CHECK_TIMEOUT_STRATEGY = {
    "auth_ban": TimeoutStrategy.FAIL_FAST,      # 关键检查,超时失败
    "auth_bot": TimeoutStrategy.FAIL_FAST,      # 关键检查,超时失败
    "auth_group": TimeoutStrategy.CONTINUE,     # 非关键,超时继续
    "auth_limit": TimeoutStrategy.CONTINUE,     # 非关键,超时继续
    "get_plugin_and_user": TimeoutStrategy.CACHE_FALLBACK,  # 使用缓存降级
}

实施优先级

高优先级(立即实施)

  1. ✅ 调整检查顺序:ban 检查优先执行
    • 风险:低
    • 收益:高
    • 工作量:小(1-2 小时)

中优先级(本周实施)

  1. ✅ 提前获取 group 数据,统一数据获取

    • 风险:低
    • 收益:高(充分利用 DataAccess 缓存)
    • 工作量:小(1-2 小时)
  2. ✅ 优化数据获取顺序,提高缓存命中率

    • 风险:低(DataAccess 已实现缓存)
    • 收益:高(减少数据库查询)
    • 工作量:小(1 小时)
  3. ✅ 添加缓存统计监控

    • 风险:低
    • 收益:中(可以监控缓存效果)
    • 工作量:小(30 分钟)

低优先级(后续优化)

  1. ⏳ 统一错误处理策略
    • 风险:中(可能影响现有逻辑)
    • 收益:中
    • 工作量:中(3-4 小时)

代码示例:改进后的 auth 函数结构

async def auth(...):
    # 1. 获取所有需要的数据(并行)
    plugin, user, group = await gather_all_data(...)

    # 2. 关键检查(串行,失败立即退出)
    await critical_checks(plugin, user, group, ...)

    # 3. 其他检查(并行)
    await other_checks(plugin, user, group, ...)

    # 4. 金币检查(最后执行,超时不影响)
    cost_gold = await check_cost(user, plugin, ...)

注意事项

  1. 向后兼容:确保改进不影响现有功能
  2. 充分测试:特别是缓存相关的改进
  3. 监控指标:记录改进前后的性能指标
  4. 逐步实施:先实施低风险改进,再实施高风险改进

预期效果

  • 性能提升:总体执行时间减少 20-30%
  • 资源节省:减少不必要的检查执行 30-50%
  • 缓存优化:充分利用 DataAccess 的 Redis 缓存,缓存命中率提升 40-60%
  • 数据库压力:减少数据库查询 30-50%(通过缓存)
  • 用户体验:响应速度提升,特别是在高并发场景下

关于 DataAccess 缓存的说明

DataAccess 已实现的缓存机制

  1. 自动缓存查询结果

    • 所有通过 DataAccess.safe_get_or_none() 等方法的查询结果都会自动缓存
    • 缓存键基于模型的主键字段(如 user_id、module 等)
  2. 空结果缓存

    • 查询结果为 None 时,会缓存一个特殊标记(_NULL_RESULT)
    • 默认 TTL 为 5 分钟,避免频繁查询不存在的记录
    • 可通过 DataAccess.set_null_result_ttl() 调整
  3. 缓存统计

    • 自动统计缓存命中率、命中次数、未命中次数等
    • 可通过 DataAccess.get_cache_stats() 获取统计信息
  4. 缓存失效

    • 数据更新、创建、删除时自动清除相关缓存
    • 支持手动清除:DataAccess.clear_cache(**kwargs)

优化建议

  1. 统一使用 DataAccess:确保所有数据访问都通过 DataAccess,而不是直接使用模型类
  2. 并行获取数据:利用 asyncio.gather() 并行获取多个数据,提高缓存命中率
  3. 监控缓存效果:定期查看 DataAccess.get_cache_stats(),了解缓存命中率
  4. 调整空结果 TTL:根据实际需求调整 DataAccess.set_null_result_ttl(),平衡缓存效果和实时性