--- title: 限流 --- # 限流 Coco 限流(`coco-rate-limit`)在 Servlet 入口处对显式声明的路由做请求配额控制。它使用**固定窗口计数器(fixed-window counter)**算法:每个限流键在一个对齐的时间窗口内累加计数,达到上限即拒绝,窗口滚动后计数归零。它不是令牌桶,也不是滑动窗口,因此在窗口边界附近可能出现短时的双倍突发,这是固定窗口算法的固有特性。 限流绑定 `coco.rate-limit` 命名空间,依赖 Web 运行时特性(`web`)。默认关闭;启用后也只有你在 `coco.rate-limit.routes` 中显式声明的路由会被拦截,不会对全部请求生效。 ## 功能简介 - **三种限流算法**:每条路由用 `algorithm` 选择固定窗口、滑动窗口或令牌桶,三者共用"`windowSeconds` 秒内允许 `limit` 次"的配置心智,默认固定窗口。详见[算法选择](#选择限流算法)。 - **两条执行路径共享同一计数语义**:路径匹配的 Servlet 过滤器(Filter)在最靠前的位置执行;`@CocoRateLimited` 注解走 MVC 拦截器后备路径。当 Filter 已按路径匹配并占用了配额时,注解拦截器不会重复扣减,避免同一请求被计两次。 - **fail-closed(失败即拒绝)**:键解析或存储发生异常、存储容量耗尽时按拒绝处理,返回 HTTP 503,而不是放行。 - **标准限流响应头**:无论放行还是拒绝,都会写出配额相关响应头,便于客户端自适应退避。 - **可替换存储与键解析**:默认进程内存储仅适合单实例;`CocoRateLimitStore` 与 `CocoRateLimitKeyResolver` 均可替换。 ## 如何启用接入 限流受两层开关控制:特性开关 `web`(限流依赖 Web 运行时)必须开启,同时需要显式打开 `coco.rate-limit.enabled`。属性默认关闭,避免升级后自动启用限流。 ### 1. 打开开关并声明路由 ```yaml coco: rate-limit: enabled: true routes: - id: login limit: 5 window-seconds: 60 matcher: methods: - POST path-patterns: - /api/auth/login - id: public-read limit: 100 window-seconds: 60 matcher: path-patterns: - /api/public/** ``` `matcher.path-patterns` 使用 Spring Ant 风格模式;`methods` 为空表示匹配所有 HTTP 方法。路由要生效必须同时满足:`id` 非空、至少一个非空 `path-pattern`、`limit > 0`、`windowSeconds` 在 1 至 366 天之间。 ### 2. (可选)用注解表达业务意图 ```java @RestController @RequestMapping("/api/auth") public class AuthController { @CocoRateLimited("login") @PostMapping("/login") public LoginResponse login(@RequestBody LoginRequest request) { // ... } } ``` `@CocoRateLimited` 只表达"该处理方法预期由某条路由保护"的意图,它**不会创建隐式路由**,也不读取用户、角色或事务状态。实际拦截规则仍由 `coco.rate-limit.routes` 显式配置。`value` 与 `route` 互为别名,可标注在类型或方法上。 ## 选择限流算法 每条路由用 `algorithm` 指定算法,默认 `fixed-window`。三者共用同一份 `limit` / `window-seconds` 配置,区别只在如何在时间上分摊额度: ```yaml coco: rate-limit: enabled: true routes: - id: payment algorithm: sliding-window # fixed-window(默认)| sliding-window | token-bucket limit: 100 window-seconds: 60 matcher: path-patterns: - /api/payment/** ``` | 算法 | 行为 | 适用 | 代价 | |------|------|------|------| | `fixed-window` | 按 `window-seconds` 对齐时间轴,每窗独立计数 | 通用、日志类,对瞬时峰值不敏感 | 两窗交界处最多放行 **2×limit**(前窗末尾 + 后窗开头) | | `sliding-window` | 当前窗口计数 + 上一窗口计数按时间加权,近似连续滑动 | **支付、秒杀**等对突发敏感的场景 | 略高(需保留上一窗口计数) | | `token-bucket` | 桶容量 `limit`,以 `limit/window-seconds` 个/秒匀速补充,每请求耗一个 | 允许可控突发、长期均速受限(如"平时低峰偶尔成组") | 与滑动窗口相当 | :::tip[固定窗口的 2× 突发] 固定窗口在窗口交界处存在放行 2 倍额度的固有缺陷:窗口 N 的最后一刻放行 `limit` 个,越过边界后窗口 N+1 立即又放行 `limit` 个,约 1 秒内实际放行 `2×limit`。支付、秒杀这类场景应选 `sliding-window` 或 `token-bucket`。 ::: Redis 存储下三种算法各由一段 Lua 脚本原子执行,计数使用 Redis 服务器时间,避免多实例时钟漂移。 ## 使用示例 ### 限流响应头 放行请求携带以下响应头(`X-` 前缀为兼容别名): ``` RateLimit-Limit: 100 RateLimit-Remaining: 87 RateLimit-Reset: 42 X-RateLimit-Limit: 100 X-RateLimit-Remaining: 87 X-RateLimit-Reset: 42 ``` `RateLimit-Reset` 是距离当前窗口重置的剩余秒数。被拒绝时额外携带 `Retry-After`(至少为 1 秒): ``` HTTP/1.1 429 Too Many Requests RateLimit-Limit: 5 RateLimit-Remaining: 0 RateLimit-Reset: 18 Retry-After: 18 Content-Type: application/json {"code":42900,"message":"..."} ``` 配额耗尽返回业务码 `42900`(HTTP 429);键解析或存储不可用、容量耗尽返回业务码 `50300`(HTTP 503)。 ### 集群部署替换共享存储 进程内存储的状态只存在于当前 JVM,多实例部署下各实例配额相互独立,等效于放大了总配额。启用时会输出多实例风险警告。生产多实例需切换到共享存储: ```yaml coco: rate-limit: enabled: true store-type: redis redis: key-prefix: "coco:rate-limit:" ``` 或提供自定义 `CocoRateLimitStore` Bean 覆盖默认实现。 ### 反向代理下的客户端识别 默认键解析器(`DefaultCocoRateLimitKeyResolver`)**只使用 Servlet 容器上报的远端地址**,绝不信任 `X-Forwarded-For` 等客户端可伪造的请求头。部署在可信反向代理之后时,需显式声明可信代理边界,解析器才会从转发链中按右向左信任边界取第一个非代理地址: ```yaml coco: rate-limit: trusted-proxy: remote-addresses: - 10.0.0.1 - 10.0.0.2 ``` ## 关键配置项 前缀 `coco.rate-limit`。 | 配置项 | 类型 | 默认值 | 说明 | |--------|------|--------|------| | `enabled` | boolean | `false` | 是否启用限流。 | | `routes` | list | 空 | 显式限流路由列表;只有列表内路由被拦截。 | | `routes[].id` | string | — | 路由标识,与 `@CocoRateLimited` 的 `route` 对应。 | | `routes[].algorithm` | enum | `fixed-window` | 限流算法:`fixed-window` / `sliding-window` / `token-bucket`。 | | `routes[].limit` | long | `100` | 窗口内允许的请求数(令牌桶下即桶容量)。 | | `routes[].window-seconds` | long | `60` | 窗口时长(秒),范围 1 至 366 天(令牌桶下即补满一桶所需秒数)。 | | `routes[].matcher.methods` | list | 空(全部方法) | 匹配的 HTTP 方法。 | | `routes[].matcher.path-patterns` | list | 空 | Ant 风格路径模式,至少一个非空才有效。 | | `store-type` | enum | `in-memory` | 存储类型,可选 `in-memory` / `redis`。 | | `in-memory.max-entries` | int | `10000` | 进程内存储最大活动限流键数。 | | `in-memory.cleanup-interval-seconds` | int | `60` | 过期键后台清理间隔(秒)。 | | `redis.key-prefix` | string | `coco:rate-limit:` | Redis 键前缀。 | | `redis.template-bean-name` | string | 空 | 指定 RedisTemplate Bean 名称,空则使用默认。 | | `filter.excluded-path-patterns` | list | `/actuator`, `/actuator/**`, `/health`, `/health/**` | Filter 跳过的路径,避免监控请求占用业务配额。 | | `trusted-proxy.remote-addresses` | list | 空 | 可信反向代理地址;空为安全默认值,不解析任何转发头。 | ## 边界注意事项 - **仅在 Servlet 应用生效**:限流依赖 `web` 特性,Filter 与 MVC 拦截器均只在 Servlet 环境注册。 - **固定窗口的边界突发**:由于窗口对齐而非滑动,相邻两个窗口交界处理论上可通过接近 `2 × limit` 的请求,对严格平滑限流的场景需自行评估。 - **注解不等于配置**:`@CocoRateLimited` 不生成路由。忘记在 `coco.rate-limit.routes` 中声明对应 `id` 时,注解不会产生任何拦截效果。 - **进程内存储不可用于集群**:多实例下务必替换为共享 `CocoRateLimitStore`,否则总配额被放大。 - **fail-closed 语义**:存储异常或容量耗尽时返回 503 而非放行。需要为共享存储做好可用性保障。 - **默认不信任转发头**:未配置 `trusted-proxy.remote-addresses` 时,代理后的所有客户端会被识别为同一个远端地址(代理地址),可能导致误限流。生产环境务必按实际拓扑配置或替换键解析器。