跳到主要内容

限流

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,而不是放行。
  • 标准限流响应头:无论放行还是拒绝,都会写出配额相关响应头,便于客户端自适应退避。
  • 可替换存储与键解析:默认进程内存储仅适合单实例;CocoRateLimitStoreCocoRateLimitKeyResolver 均可替换。

如何启用接入

限流受两层开关控制:特性开关 web(限流依赖 Web 运行时)必须开启,同时需要显式打开 coco.rate-limit.enabled。属性默认关闭,避免升级后自动启用限流。

1. 打开开关并声明路由

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-patternlimit > 0windowSeconds 在 1 至 366 天之间。

2. (可选)用注解表达业务意图

@RestController
@RequestMapping("/api/auth")
public class AuthController {

@CocoRateLimited("login")
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest request) {
// ...
}
}

@CocoRateLimited 只表达"该处理方法预期由某条路由保护"的意图,它不会创建隐式路由,也不读取用户、角色或事务状态。实际拦截规则仍由 coco.rate-limit.routes 显式配置。valueroute 互为别名,可标注在类型或方法上。

选择限流算法

每条路由用 algorithm 指定算法,默认 fixed-window。三者共用同一份 limit / window-seconds 配置,区别只在如何在时间上分摊额度:

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-windowwindow-seconds 对齐时间轴,每窗独立计数通用、日志类,对瞬时峰值不敏感两窗交界处最多放行 2×limit(前窗末尾 + 后窗开头)
sliding-window当前窗口计数 + 上一窗口计数按时间加权,近似连续滑动支付、秒杀等对突发敏感的场景略高(需保留上一窗口计数)
token-bucket桶容量 limit,以 limit/window-seconds 个/秒匀速补充,每请求耗一个允许可控突发、长期均速受限(如"平时低峰偶尔成组")与滑动窗口相当
固定窗口的 2× 突发

固定窗口在窗口交界处存在放行 2 倍额度的固有缺陷:窗口 N 的最后一刻放行 limit 个,越过边界后窗口 N+1 立即又放行 limit 个,约 1 秒内实际放行 2×limit。支付、秒杀这类场景应选 sliding-windowtoken-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,多实例部署下各实例配额相互独立,等效于放大了总配额。启用时会输出多实例风险警告。生产多实例需切换到共享存储:

coco:
rate-limit:
enabled: true
store-type: redis
redis:
key-prefix: "coco:rate-limit:"

或提供自定义 CocoRateLimitStore Bean 覆盖默认实现。

反向代理下的客户端识别

默认键解析器(DefaultCocoRateLimitKeyResolver只使用 Servlet 容器上报的远端地址,绝不信任 X-Forwarded-For 等客户端可伪造的请求头。部署在可信反向代理之后时,需显式声明可信代理边界,解析器才会从转发链中按右向左信任边界取第一个非代理地址:

coco:
rate-limit:
trusted-proxy:
remote-addresses:
- 10.0.0.1
- 10.0.0.2

关键配置项

前缀 coco.rate-limit

配置项类型默认值说明
enabledbooleanfalse是否启用限流。
routeslist显式限流路由列表;只有列表内路由被拦截。
routes[].idstring路由标识,与 @CocoRateLimitedroute 对应。
routes[].algorithmenumfixed-window限流算法:fixed-window / sliding-window / token-bucket
routes[].limitlong100窗口内允许的请求数(令牌桶下即桶容量)。
routes[].window-secondslong60窗口时长(秒),范围 1 至 366 天(令牌桶下即补满一桶所需秒数)。
routes[].matcher.methodslist空(全部方法)匹配的 HTTP 方法。
routes[].matcher.path-patternslistAnt 风格路径模式,至少一个非空才有效。
store-typeenumin-memory存储类型,可选 in-memory / redis
in-memory.max-entriesint10000进程内存储最大活动限流键数。
in-memory.cleanup-interval-secondsint60过期键后台清理间隔(秒)。
redis.key-prefixstringcoco:rate-limit:Redis 键前缀。
redis.template-bean-namestring指定 RedisTemplate Bean 名称,空则使用默认。
filter.excluded-path-patternslist/actuator, /actuator/**, /health, /health/**Filter 跳过的路径,避免监控请求占用业务配额。
trusted-proxy.remote-addresseslist可信反向代理地址;空为安全默认值,不解析任何转发头。

边界注意事项

  • 仅在 Servlet 应用生效:限流依赖 web 特性,Filter 与 MVC 拦截器均只在 Servlet 环境注册。
  • 固定窗口的边界突发:由于窗口对齐而非滑动,相邻两个窗口交界处理论上可通过接近 2 × limit 的请求,对严格平滑限流的场景需自行评估。
  • 注解不等于配置@CocoRateLimited 不生成路由。忘记在 coco.rate-limit.routes 中声明对应 id 时,注解不会产生任何拦截效果。
  • 进程内存储不可用于集群:多实例下务必替换为共享 CocoRateLimitStore,否则总配额被放大。
  • fail-closed 语义:存储异常或容量耗尽时返回 503 而非放行。需要为共享存储做好可用性保障。
  • 默认不信任转发头:未配置 trusted-proxy.remote-addresses 时,代理后的所有客户端会被识别为同一个远端地址(代理地址),可能导致误限流。生产环境务必按实际拓扑配置或替换键解析器。