
前两篇搭好了管道和插件机制这篇把限流从“能调阈值”推进到“能调维度”默认按接口限流四行注解按参数限流一个 Bean 按用户/IP/租户任意组合限流——最后讲清楚「不同维度各解决什么问题、各自有什么安全边界」。第一篇把接口治理做成了管道第二篇讲了怎么写一个敢上生产的插件。这篇兑现第二篇结尾的预告讲管道上那个最有商业价值的扩展点——限流键解析器RateLimitKeyResolver。很多团队做限流做的其实是「调阈值」加注解、配limit、收工。这个够解决“接口被刷了”——但解决不了“只刷了一个用户”、“租户 A 把租户 B 的配额吃完了”、“登录接口被爆破但全局阈值调低会误伤正常用户”这类问题。这些问题的区别不在算法在“以什么维度限流”——而这正是键解析器这个扩展点存在的意义。一、默认长什么样接口级限流先理解框架不开箱就给的默认值。RateLimit(limit 100)贴在一个 Controller 方法上同一个接口的所有请求共享同一份配额不管是谁发起的PostMappingRateLimit(limit100,window60)publicOrdercreate(RequestBodyOrderRequestreq){...}限流键 全限定类名#方法名比如com.example.OrderController#create。同一个键同一份配额。一个用户连刷 100 次第 101 次被拒——不管这个用户是 VIP 还是爬虫。够用吗对于“接口被并发刷爆”这个场景够。对于“一个恶意用户疯狂刷单”这个场景不够——把全局阈值调高会拖垮接口调低会误伤正常用户。区别就是维度。二、四行注解升级SpEL 参数维度限流最常见的维度需求是“按参数”同一个接口不同的参数值各自独立计数。注解上直接写 SpELGetMapping(/users/{id})RateLimit(limit10,window60,key#id)publicUserget(PathVariableLongid){...}PostMapping(/login)RateLimit(limit5,window60,key#request.username)publicTokenlogin(RequestBodyLoginRequestrequest){...}最终限流键 全限定类名#方法名:表达式结果。id42 和 id43 各自 10 次互不相干。实现上用受限的SimpleEvaluationContext求值不允许类型引用、构造器调用和 Bean 引用表达式失败自动回退接口级限流只打 warn。这套防御是为了杜绝 SpEL 注入——StandardEvaluationContext加上客户端可控输入是可以直接打出 RCE 的。但参数维度有一个安全边界上一篇提过这里必须再说一遍当表达式结果由客户端可控输入派生时每个新取值都从满配额开始。攻击者遍历一万个不同的id等于拥有一万个独立 10 次配额——所以参数维度的正确用途是租户间公平性防止一个租户耗尽共享配额它不能作为防爆破等安全边界使用防爆破应叠加接口级总配额或者用下面的RateLimitKeyResolver组合 IP 等低基数维度。三、正餐一个 Bean四种真实业务的键玩法当 SpEL 搞不定需要请求头、安全上下文、组合维度时就该上RateLimitKeyResolver了。它是FunctionalInterface注册成 Bean 就自动覆盖默认实现FunctionalInterfacepublicinterfaceRateLimitKeyResolver{Stringresolve(FilterContextcontext);}约定相同键共享同一份配额不同键互不相干。返回值就是 Redis 里的 key因此本地/Redis 限流都生效。玩法 1按用户限流VIP 与恶意用户分开算从请求头或安全上下文取用户 ID键追加#user:后缀BeanpublicRateLimitKeyResolveruserRateLimitKeyResolver(){returncontext-{StringuserIdcontext.getAttribute(X-User-Id,anonymous);// 匿名用户共用一个键兜底登录用户各自独立returncontext.getApiKey()#user:userId;};}效果user:42刷到第 101 次被拒user:43第一次还能放——接口级总配额和用户级配额各自独立恶意用户刷不完别人的。注意不要把整个接口都切到纯用户级。如果用户 ID 不存在时也要限流一定要给匿名流量一个独立键上面用了anonymous否则匿名流量会共享全局配额等于退回了接口级。玩法 2按 IP 限流登录接口防爆破登录、注册、找回密码这类接口需要的是“同一个 IP 每秒最多试几次”。IP 从上下文拿框架已按X-Forwarded-For→X-Real-IP→remoteAddr解析好BeanpublicRateLimitKeyResolveripRateLimitKeyResolver(){returncontext-context.getApiKey()#ip:context.getClientIp();}但这里有个坑必须说出来clientIp可以被X-Forwarded-For伪造。按 IP 限流可以用来“挡懒虫”正常用户不会伪造 IP但不能把它当安全边界——把按 IP 限流当唯一防线攻击者换一行 header 就绕过了。防爆破应该是「接口级总配额 IP 级配额」的组合接口级保证整个登录接口不崩IP 级挡住单个 IP 的爆破尝试。更稳妥的组合键BeanpublicRateLimitKeyResolverloginBruteForceResolver(){returncontext-{Stringipcontext.getClientIp();// 接口级总配额ip null 时共享 单 IP 独立配额防穿透returncontext.getApiKey()(ipnull?:#ip:ip);};}玩法 3按租户限流多租户公平性这是「按参数限流」的正确归宿——拿租户 ID 作为键的一部分让租户之间互不影响BeanpublicRateLimitKeyResolvertenantRateLimitKeyResolver(){returncontext-{StringtenantIdcontext.getAttribute(X-Tenant-Id,default);returncontext.getApiKey()#tenant:tenantId;};}关键区别租户 ID 通常是服务端签发的JWT 里带的、网关写进 header 的不像 SpEL 参数那样客户端可控——所以它可以作为安全边界使用。租户 A 刷到配额用完租户 B 正常访问这正是“公平性”的本意。但要提醒一点高基数键租户多时要注意本地限流器的max-entries上限超限会淘汰旧键——如果你按租户限流一定要评估租户数量会不会超过这个上限超限的租户键被淘汰后等于“重新满血”配额就失效了。玩法 4组合维度——「接口 用户 租户」真实业务常常是多维的。键本身没有结构限制你可以任意拼接BeanpublicRateLimitKeyResolvercompositeRateLimitKeyResolver(){returncontext-{StringuserIdcontext.getAttribute(X-User-Id,anon);StringtenantIdcontext.getAttribute(X-Tenant-Id,default);// 同一用户在同一租户下访问同一接口才共享配额returncontext.getApiKey()#tenant:tenantId#user:userId;};}组合维度唯一要注意的键越细内存/Redis 里的 key 越多。本地限流的max-entries和 Redis 的内存规划都要按“键的数量 接口数 × 用户数 × 租户数”的乘积来评估别到线上才发现键爆了。四、附带福利限流拒绝响应头从 0.5.x 开始被限流的请求会自动带上 IETF 草案的RateLimit-*标准响应头RateLimit-Limit: 100 RateLimit-Remaining: 0 RateLimit-Reset: 60 Retry-After: 60客户端可以据此做智能重试等到Retry-After秒后再试不用解析文本错误信息。想自定义响应头或响应体注册RateLimitRejectHandlerBean 即可覆盖它抛出异常会自动回退默认行为不影响短路语义。五、选型速查哪种业务该用哪个维度业务场景推荐维度实现方式安全边界接口防刷、防 QPS 击穿接口级默认RateLimit(limit...)✅ 是不同参数独立配额SpEL 参数级RateLimit(key #id)❌ 否仅公平性登录/注册防爆破接口级 IP 级组合RateLimitKeyResolver⚠️ IP 可伪造需组合VIP 用户与爬虫分开算用户级RateLimitKeyResolver⚠️ 匿名用户要兜底多租户公平性租户级RateLimitKeyResolver✅ 是租户 ID 服务端签发三者组合接口用户租户RateLimitKeyResolver视组合而定六、写在最后限流从“注解调阈值”到“解析器调维度”是治理能力从“能用”到“敢上生产”的分水岭。维度选错阈值调得再准也白搭——接口级是兜底参数级是公平用户级是区分租户级是隔离。完整代码和文档https://github.com/BIGLV666/api-governance-spring-boot-starter评论区聊聊你们线上用的什么限流维度有没有踩过“维度选错”的坑下一篇打算拆告警风暴抑制——同一个接口被限流一万次怎么保证钉钉群里只收到一条消息关注不迷路。