diff --git a/openspec/changes/update-rate-limit-ttl-atomic/design.md b/openspec/changes/update-rate-limit-ttl-atomic/design.md deleted file mode 100644 index 55be897a..00000000 --- a/openspec/changes/update-rate-limit-ttl-atomic/design.md +++ /dev/null @@ -1,37 +0,0 @@ -## Context -限流中间件当前采用 `INCR` 后 `EXPIRE` 的两步操作,且未处理 `EXPIRE` 失败,导致计数 key 可能没有过期时间。该情况一旦发生,计数会持续累加,触发长期限流并造成 Redis key 膨胀。 - -## Goals / Non-Goals -- Goals: - - 原子化 Redis 计数与过期设置 - - 修复 TTL 缺失的历史 key - - 支持按接口配置 Redis 故障策略(fail-open/fail-close) - - 为需要强制保护的接口启用 fail-close -- Non-Goals: - - 改变现有固定窗口限流算法 - - 调整限流 key 格式或前缀 - - 引入新的外部依赖 - -## Decisions -- 使用 Lua 脚本在 Redis 内部原子执行 `INCR`、`TTL` 与 `PEXPIRE` -- 过期时间统一采用毫秒精度窗口(`window.Milliseconds()` 向下取整)以保持精度一致 -- 当毫秒窗口小于 1 时,按 1ms 设置过期,避免 0 导致立即过期 -- 当 `count == 1` 或 `TTL == -1` 时设置过期,避免刷新已有 TTL -- 新增 `RateLimitOptions` 并提供 `LimitWithOptions`,由调用方显式配置故障策略 -- `Limit` 默认使用 fail-open 以保持兼容 -- 当 fail-close 生效时,Redis 执行失败直接返回 429 - -## Alternatives considered -- 使用 `MULTI/EXEC` 事务封装 `INCR` + `EXPIRE`:原子性可保证,但无法在同一事务内便捷修复 `TTL == -1`,且仍需额外判断逻辑 -- 使用 `SET` + `EX`/`NX` 组合:无法保留计数累加语义 - -## Risks / Trade-offs -- Lua 脚本会带来轻微 CPU 开销,但可接受 -- TTL 修复会在首次访问时设定过期,可能缩短历史脏 key 的“无限期”状态,这是期望的修复效果 - -## Migration Plan -- 上线后脚本在请求路径上自动修复 TTL 缺失的 key -- 如需回滚,恢复原有两步命令即可 - -## Open Questions -- 无 diff --git a/openspec/changes/update-rate-limit-ttl-atomic/proposal.md b/openspec/changes/update-rate-limit-ttl-atomic/proposal.md deleted file mode 100644 index ba19528d..00000000 --- a/openspec/changes/update-rate-limit-ttl-atomic/proposal.md +++ /dev/null @@ -1,14 +0,0 @@ -# Change: 原子化 Redis 限流 TTL 设置 - -## Why -当前限流逻辑使用 `INCR` 后再 `EXPIRE`,非原子且未处理 `EXPIRE` 失败,会导致 key 可能永久存在,引发长期限流或 Redis 内存增长。 - -## What Changes -- 使用 Lua 脚本原子化 `INCR` + 过期设置 -- 当检测到 TTL 缺失时补设过期,修复历史脏数据 -- 支持 Redis 故障策略配置(默认放行,特定接口可 fail-close) -- 新增 `limit-requests` capability,用于描述限流行为与故障策略 - -## Impact -- Affected specs: 新增 `specs/limit-requests/spec.md` -- Affected code: `backend/internal/middleware/rate_limiter.go` diff --git a/openspec/changes/update-rate-limit-ttl-atomic/specs/limit-requests/spec.md b/openspec/changes/update-rate-limit-ttl-atomic/specs/limit-requests/spec.md deleted file mode 100644 index e6f9b73c..00000000 --- a/openspec/changes/update-rate-limit-ttl-atomic/specs/limit-requests/spec.md +++ /dev/null @@ -1,41 +0,0 @@ -## ADDED Requirements -### Requirement: 原子化限流计数与过期 -限流中间件 SHALL 在单个原子操作中完成 Redis 计数增量与过期设置,并且仅在首次创建或 TTL 缺失时设置过期,避免刷新已有 TTL;过期时间以毫秒为单位向下取整,最小为 1ms。 - -#### Scenario: 首次请求创建计数器 -- **WHEN** 第一次请求命中该限流 key -- **THEN** 计数增量为 1 且 key 过期时间设置为窗口值 - -#### Scenario: 窗口小于 1ms -- **WHEN** 限流窗口小于 1ms -- **THEN** 过期时间按 1ms 设置 - -#### Scenario: 窗口包含非整数毫秒 -- **WHEN** 限流窗口包含非整数毫秒 -- **THEN** 过期时间按毫秒向下取整 - -#### Scenario: 已有 TTL 的计数器继续计数 -- **WHEN** 计数器已存在且 TTL 正常 -- **THEN** 计数递增且 TTL 不被刷新 - -#### Scenario: 计数器缺失 TTL -- **WHEN** 计数器存在但 TTL 为 -1 -- **THEN** 系统为该 key 补设窗口过期时间 - -### Requirement: Redis 故障策略可配置 -限流中间件 SHALL 支持为每个限流 key 配置 Redis 故障策略,支持 fail-open 与 fail-close,默认 fail-open,配置由调用方在注册限流时提供。 - -#### Scenario: fail-open 策略 -- **WHEN** 配置为 fail-open 且 Redis 脚本执行返回错误或连接不可用 -- **THEN** 请求继续处理且不执行限流阻断 - -#### Scenario: fail-close 策略 -- **WHEN** 配置为 fail-close 且 Redis 脚本执行返回错误或连接不可用 -- **THEN** 请求被限流阻断并返回 429 - -### Requirement: 优惠码验证接口 fail-close -系统 SHALL 对 `/auth/validate-promo-code` 的限流在 Redis 故障时采用 fail-close。 - -#### Scenario: 验证优惠码时 Redis 不可用 -- **WHEN** 请求 `/auth/validate-promo-code` 且 Redis 不可用 -- **THEN** 请求返回 429 diff --git a/openspec/changes/update-rate-limit-ttl-atomic/tasks.md b/openspec/changes/update-rate-limit-ttl-atomic/tasks.md deleted file mode 100644 index c033ca95..00000000 --- a/openspec/changes/update-rate-limit-ttl-atomic/tasks.md +++ /dev/null @@ -1,6 +0,0 @@ -## 1. Implementation -- [x] 1.1 在限流中间件中引入 Lua 脚本原子化计数与过期设置(使用 PEXPIRE 毫秒窗口) -- [x] 1.2 脚本内检测 `TTL == -1` 时补设过期,修复历史脏 key -- [x] 1.3 引入 `RateLimitOptions` 与 `LimitWithOptions`,`Limit` 保持默认 fail-open -- [x] 1.4 为 `/auth/validate-promo-code` 配置 fail-close 策略 -- [x] 1.5 添加测试覆盖首次请求、已有 TTL、TTL 缺失、非整数毫秒窗口与故障策略(使用 Redis 集成测试/testcontainers 方案)