Spring Cloud Gateway编程式用RetryGatewayFilter遇Netty引用计数异常
这个异常的根源其实很好理解:你用了readBody和modifyRequestBody过滤器,这两个操作会直接消费请求体——而Netty的ByteBuf是基于引用计数管理的,消费完之后引用计数会变成0。当Retry过滤器触发重试时,它会尝试再次读取请求体,这时候就会因为引用计数已经耗尽而抛出IllegalReferenceCountException。哪怕是GET请求,Spring Cloud Gateway在处理流程中也可能会创建空的请求体对象,同样会遇到这个问题。
这里给你几个可行的解决办法:
1. 缓存请求体(最稳妥的方案)
在重试过滤器之前添加cacheRequestBody过滤器,它会把请求体缓存起来,这样每次重试都能复用缓存的副本,不会出现引用计数耗尽的问题。修改你的路由配置代码如下:
route("route-one", p -> p .readBody(String.class, i -> !StringUtils.isEmpty(i)).and() .path("/id") .filters(f -> f .cacheRequestBody(String.class) // 新增:缓存请求体 .modifyRequestBody(String.class, String.class, ((exchange, s) -> { // 你的请求体修改逻辑 return Mono.just(s); // 示例返回,根据实际逻辑调整 })) .retry(retryConfig -> { retryConfig.setRouteId("route-one"); RetryGatewayFilterFactory.BackoffConfig backoffConfig = new RetryGatewayFilterFactory.BackoffConfig(); backoffConfig.setFirstBackoff(Duration.ofSeconds(1)); backoffConfig.setMaxBackoff(Duration.ofSeconds(5)); backoffConfig.setFactor(1); backoffConfig.setBasedOnPreviousValue(true); retryConfig.setSeries(HttpStatus.Series.SERVER_ERROR); retryConfig.setStatuses(HttpStatus.BAD_GATEWAY, HttpStatus.INTERNAL_SERVER_ERROR); retryConfig.setRetries(3); retryConfig.setBackoff(backoffConfig); // 现在可以正常启用退避配置了 }) ) .uri(Url))
2. 调整过滤器顺序
确保cacheRequestBody在readBody和modifyRequestBody之前执行,这样缓存的是原始请求体,后续的修改和重试操作都能基于缓存内容进行,避免重复消费导致的引用计数问题。
3. 避免提前消费请求体(可选)
如果你的业务逻辑允许,尽量不要用readBody来判断请求体是否为空。可以改用直接从ServerHttpRequest的getBody()获取流,但要注意不要提前消费整个流——或者在需要检查的时候,把流复制一份再处理,这样原始流还能留给重试使用。不过这种方式相对复杂,不如缓存请求体来得直接。
总结一下,核心就是要保证重试时请求体是可重复读取的,cacheRequestBody是Spring Cloud Gateway提供的专门解决这类问题的过滤器,推荐优先使用这个方案。
内容的提问来源于stack exchange,提问作者manojVsm

