Skip to content

feat: 为分组/用户新增 RPM 限流(分组优先,用户兜底)#1743

Closed
james-6-23 wants to merge 1 commit intoWei-Shaw:mainfrom
james-6-23:main
Closed

feat: 为分组/用户新增 RPM 限流(分组优先,用户兜底)#1743
james-6-23 wants to merge 1 commit intoWei-Shaw:mainfrom
james-6-23:main

Conversation

@james-6-23
Copy link
Copy Markdown
Contributor

变更说明

本次新增能力:为上游请求加入两级 RPM(每分钟请求数)限流,分别作用于分组与用户,采用 fallback 语义——分组设了 rpm_limit
就按分组限;分组没设,则按用户级 rpm_limit 兜底;两者都为 0 则不限流。

背景与动机

  • 管理员此前只能通过"并发数 / 计费上限"约束用户,无法对单位时间内的请求数做强约束
  • 没有 RPM 就容易出现:
    • 某个用户短时间爆发式打满上游,影响同分组其他用户
    • 同一用户创建多个 API Key 绕过任何以 api_key 为维度的限制
  • 期望的语义:分组管理员统一控制 > 用户级兜底。既保留分组内一致的策略,又允许对未设分组配额的场景做账号级限速。

设计决策

限流判定采用 fallback,不做两层独立叠加(避免"取较小者"带来的认知负担):

┌────────────────┬───────────────┬────────────────┐
│ Group.RPMLimit │ User.RPMLimit │ 生效 RPM │
├────────────────┼───────────────┼────────────────┤
│ > 0 │ 任意 │ 按分组 │
├────────────────┼───────────────┼────────────────┤
│ 0 │ > 0 │ 按用户(兜底) │
├────────────────┼───────────────┼────────────────┤
│ 0 │ 0 │ 不限流 │
└────────────────┴───────────────┴────────────────┘

计数粒度按 user 聚合(不是 api_key),杜绝"同一用户建多个 Key 绕过限流"的路径。

修复内容

后端

  • Schema & Migrations
    • migrations/108_add_group_rpm_limit.sql:groups.rpm_limit int DEFAULT 0
    • migrations/109_add_user_rpm_limit.sql:users.rpm_limit int DEFAULT 0
    • ent schema 同步,重新生成 ent 代码
  • 限流引擎
    • 新增 service.UserRPMCache 接口与 repository.UserRPMCacheImpl
    • Redis 键:rpm:ug:{userID}:{groupID}:{minute} 与 rpm:u:{userID}:{minute}
    • TxPipeline(INCR + EXPIRE(120s)) 原子计数;rdb.Time() 对齐分钟窗口;Redis 故障一律 fail-open(打 warning,不阻塞业务)
    • BillingCacheService.checkRPM:fallback 判定,放在所有前置校验(余额 / 订阅 / API Key 限额)之后执行,避免为注定失败的请求计数
  • 错误与响应
    • 新增 ErrGroupRPMExceeded(GROUP_RPM_EXCEEDED)、ErrUserRPMExceeded(USER_RPM_EXCEEDED)
    • gateway_handler.billingErrorDetails 映射到 429 rate_limit_exceeded
  • 认证快照
    • APIKeyAuthGroupSnapshot.RPMLimit / APIKeyAuthUserSnapshot.RPMLimit
    • apiKeyAuthSnapshotVersion bump v5 → v7
    • 用户 / 分组 RPM 变更触发 auth cache 失效,下次请求重建快照
  • 系统设置
    • 新增 default_user_rpm_limit(SystemSettings.DefaultUserRPMLimit)
    • 初始化 / Save / Get / 审计日志 / SystemSettings DTO 全部对齐
    • admin.CreateUser、auth.Register、两处 OAuth 自动注册路径:若未显式指定 rpm_limit,套用系统默认值

前端

  • 分组编辑(GroupsView):创建 / 编辑表单新增 rpm_limit 输入项;类型 AdminGroup.rpm_limit?: number
  • 用户编辑(UserCreateModal / UserEditModal):新增 rpm_limit 输入项,编辑时回填;类型 User.rpm_limit?: number
  • 系统设置(SettingsView):"用户默认设置"卡片新增"默认用户 RPM 限制"输入项;SystemSettings / UpdateSettingsRequest 同步
  • i18n:zh / en 全量补齐新键

验证结果

后端

go build ./... # ✅
go vet ./... # ✅
go test ./internal/service/... ./internal/handler/... ./internal/repository/... # ✅

前端

vue-tsc --noEmit # ✅

覆盖的风险场景

  • 分组设 RPM,不同用户独立计数,互不干扰(按 user, group 聚合)
  • 同一用户用多个 API Key 打同一分组,仍落在同一计数器(不被绕过)
  • 分组未设 RPM、用户设了 RPM → 按用户级兜底
  • 两者都为 0 → 不计数、不限流
  • Redis 故障 → fail-open,打 warning,不阻塞业务
  • 管理员修改用户 rpm_limit → auth cache 失效,下次请求立即生效
  • 管理员 / 注册 / OAuth 三条创建路径均会套用系统默认 RPM

影响范围

  • 热路径:新增一次 Redis TxPipeline(INCR + EXPIRE + TIME),仅当 RPMLimit > 0 时触发;未开启时零开销
  • 数据模型:groups / users 各新增一个 int DEFAULT 0 列,兼容存量数据(全部不限流)
  • 缓存兼容:快照版本从 v5 直接跳到 v7(一次升级覆盖 Group + User 两侧字段),旧快照会按版本失效重建,无需人工清理
  • API 兼容:
    • POST/PUT /admin/groups 与 POST/PUT /admin/users 新增可选 rpm_limit 字段;不传等同于"不变"
    • PUT /admin/settings 新增可选 default_user_rpm_limit
  • 业务语义:默认值(0)行为等价于改动前,不影响既有流量;仅在管理员主动配置后才启用

- 新增 Group.RPMLimit 与 User.RPMLimit 字段(migrations 108/109),
  限流计数以 (user, group) 或 user 聚合,杜绝多 Key 绕过
- BillingCacheService.checkRPM 采用 fallback 语义:分组设置 rpm_limit 则按分组计数,
  否则回落到用户级;任一超限返回 429 并区分 GROUP_RPM_EXCEEDED / USER_RPM_EXCEEDED
- 新增 UserRPMCache(Redis 键 rpm:ug:{uid}:{gid}:{min} 与 rpm:u:{uid}:{min}),
  以 TxPipeline(INCR+EXPIRE) 原子计数,Redis 服务端时间对齐分钟窗口,故障一律 fail-open
- APIKeyAuthSnapshot 快照带上 Group/User RPMLimit,版本 bump 至 v7,
  用户级变更会触发 auth cache 失效
- 系统设置新增 default_user_rpm_limit:管理员 / 自助注册 / OAuth 自动注册
  未显式指定 rpm_limit 时自动套用
- 前端:分组编辑、用户创建/编辑、系统设置"用户默认设置"均新增 RPM 输入项
  及中英文文案
@Wei-Shaw Wei-Shaw force-pushed the main branch 4 times, most recently from 1e0d466 to 4d0483f Compare April 22, 2026 10:12
@james-6-23 james-6-23 closed this Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant