Resilience4j RateLimiter异常:同时触发RequestNotPermitted与429错误
疑问1:为何场景2同时出现本地限流异常与远程API的429错误?
这是因为你使用的Resilience4j 1.7.0的RateLimiter默认实现(SemaphoreRateLimiter)的特性导致的:
- 它基于许可池机制工作:每个刷新周期(1秒)会补充6个许可,但如果请求处理完成后快速释放许可,这些许可会被新的请求立即复用。
- 场景2中15线程在1秒内逐步启动,当第一批6个请求快速处理完成(比如OAuth2 token已缓存、Feign请求耗时短),许可被释放,后续请求会拿到新的许可并发送到远程API,导致远程API在1秒内收到超过6个请求,触发其自身的限流规则返回429。
- 同时,仍有部分请求因无法获取许可被本地限流拦截,抛出
RequestNotPermitted异常。
而场景1是8线程瞬间启动,所有请求同时竞争6个许可,2个请求直接被拦截,剩下6个请求同时处理,即使处理完成,剩余时间内没有新的请求(因为JMeter循环1次),所以远程API只会收到6个请求,不会触发429。
疑问2:为何RateLimiter仅在一定并发量下有效,超过后允许超过6请求/秒的调用?
核心原因还是SemaphoreRateLimiter的设计逻辑:
它的limitForPeriod参数是每个刷新周期补充的许可数量,而非严格限制每个周期内的最大请求数。如果请求处理速度足够快(比如耗时远小于1/6秒),许可会被快速释放并复用,导致一个刷新周期(1秒)内实际处理的请求数超过6个。
你的单元测试之所以正常,是因为测试中请求是批量提交后同步等待,请求处理过程中许可不会被提前释放,所以严格控制了每个周期的请求数;但实际压测中请求是逐步到达且处理快速,许可复用导致了超量请求。
解决方案
要实现严格的每秒6请求的速率限制,适配远程API的规则,可以采用以下方案:
1. 切换到Resilience4j 2.x版本的RateLimiter
Resilience4j 2.x重构了RateLimiter实现,新增了基于滑动窗口的限流策略,能够严格控制单位时间内的请求数。升级后配置如下:
resilience4j: ratelimiter: instances: user-throttling: limit-for-period: 6 limit-refresh-period: 1s timeout-duration: 0 # 指定使用滑动窗口实现 strategy: SLIDING_WINDOW
2. 在1.x版本中结合Bulkhead控制并发
如果无法升级版本,可以用Resilience4j的Bulkhead(舱壁)限制并发请求数,结合RateLimiter来间接控制速率:
- 配置Bulkhead限制最大并发数为6,这样同一时间最多6个请求在处理。
- 若请求平均处理时间超过167ms(1/6秒),则1秒内最多处理6个请求,刚好匹配远程API规则。
配置示例:
resilience4j: bulkhead: instances: user-bulkhead: max-concurrent-calls: 6 ratelimiter: instances: user-throttling: limitForPeriod: 6 limitRefreshPeriod: 1s timeoutDuration: 0
然后在Feign接口方法上同时添加注解:
@FeignClient(...) public interface UserClient { @GetMapping(value = "/users", produces = APPLICATION_JSON_VALUE) @RateLimiter(name = "user-throttling") @Bulkhead(name = "user-bulkhead") List<JsonNode> getUsers(); }
3. 改用Guava RateLimiter
如果Resilience4j的实现不符合需求,可以集成Guava的RateLimiter,它基于令牌桶算法,能严格控制速率:
- 定义一个RateLimiter Bean:
@Configuration public class RateLimiterConfig { @Bean("userRateLimiter") public RateLimiter userRateLimiter() { return RateLimiter.create(6.0); // 每秒6个请求 } }
- 在Feign客户端的方法中通过AOP切面实现限流:
@FeignClient(...) public interface UserClient { @GetMapping(value = "/users", produces = APPLICATION_JSON_VALUE) List<JsonNode> getUsers(); } // 自定义切面 @Aspect @Component public class RateLimitAspect { @Autowired @Qualifier("userRateLimiter") private RateLimiter rateLimiter; @Around("execution(* com.yourpackage.UserClient.getUsers(..))") public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable { rateLimiter.acquire(); return joinPoint.proceed(); } }
内容的提问来源于stack exchange,提问作者Filip Kowalski

