You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot非首次请求报500,第三方权限服务调用异常排查

问题分析与解决方案

问题现象

  • 从Angular向后端发起两个调用同一第三方权限服务的请求(如getBooks、getAuthors)时,首次请求返回200,其余请求始终返回500,错误原因是响应体为空导致的序列化映射失败。
  • 极少数情况下两个请求都能返回200。
  • 直接从Angular调用第三方服务、用Postman发起100次请求均全部成功。
  • 尝试过@Async注解和异步Apache Client,问题依旧。
  • 更换其他第三方服务后,所有请求均正常返回200,初步怀疑原第三方服务存在问题。
  • 当前临时解决方案是给调用第三方服务的方法添加synchronized关键字,但团队认为这不是合理方案,寻求替代方案。

相关代码

JwtAuthenticationFilter 代码

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    String token = getTokenFromRequest(request);
    if(StringUtils.hasText(token) && jwtService.validateToken(token)){
        String username = jwtService.getUserName(token);
         // 问题出在这里上方
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
       
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                username,
                null,
                userDetails.getAuthorities()
        );

        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
filterChain.doFilter(request, response);
    
}

第三方服务调用方法代码

SimpleHttpRequest request = SimpleRequestBuilder.get(url).addParameter("username",username)
         .addHeader("Content-Type","application/json").addHeader("Accept","*/*").build();
 CloseableHttpAsyncClient client = HttpAsyncClients.custom()
        .build();
 client.start();
 Future<SimpleHttpResponse> future = client.execute(request, null);
 SimpleHttpResponse response = future.get();
ObjectMapper objectMapper = new ObjectMapper();
String code = String.valueOf(response.getCode());
List<UserAuthorityDto> authorities=objectMapper.readValue(response.getBody().getBodyText(), UserAuthorityDto.class).getAuthorities()
        .stream().collect(Collectors.toList());
 client.close();
return   CompletableFuture.completedFuture(yetkiler);

问题根源分析

  1. 第三方服务的并发/会话限制:原第三方服务可能对同一客户端的并发请求有限制,或存在会话绑定逻辑——后端同时发起的两个请求被识别为同一会话的重复请求,直接返回空响应。而Postman、Angular直接调用时,请求上下文(如Cookie、请求头标识)与后端调用不同,避开了这个限制。
  2. HTTP客户端资源未复用:当前代码每次调用都新建并销毁CloseableHttpAsyncClient,没有复用连接池。频繁创建TCP连接可能触发第三方服务的短连接限制,并发请求时第二个请求的连接未正常建立,导致空响应。
  3. 同步阻塞放大问题:虽然使用了异步客户端,但future.get()是同步阻塞调用,结合未复用的客户端资源,进一步加剧了并发请求时的连接不稳定问题。

解决方案

方案1:复用HTTP异步客户端连接池

将CloseableHttpAsyncClient配置为单例Bean,复用连接池,避免频繁创建连接触发第三方服务限制:

// 配置类中定义单例客户端
@Bean
public CloseableHttpAsyncClient httpAsyncClient() {
    return HttpAsyncClients.custom()
            .setMaxConnTotal(20)
            .setMaxConnPerRoute(10)
            .build();
}

// 调用方法中注入复用客户端
@Autowired
private CloseableHttpAsyncClient httpAsyncClient;

public CompletableFuture<List<UserAuthorityDto>> getAuthorities(String username) throws Exception {
    SimpleHttpRequest request = SimpleRequestBuilder.get(url)
            .addParameter("username", username)
            .addHeader("Content-Type", "application/json")
            .addHeader("Accept", "*/*")
            .build();

    // 客户端已在项目启动时初始化,无需每次调用start
    Future<SimpleHttpResponse> future = httpAsyncClient.execute(request, null);
    SimpleHttpResponse response = future.get();

    // 增加异常判断
    if (response.getCode() != 200) {
        throw new RuntimeException("第三方服务返回异常状态码: " + response.getCode());
    }
    String bodyText = response.getBody().getBodyText();
    if (StringUtils.isEmpty(bodyText)) {
        throw new RuntimeException("第三方服务返回空响应");
    }

    ObjectMapper objectMapper = new ObjectMapper();
    UserAuthorityDto authorityDto = objectMapper.readValue(bodyText, UserAuthorityDto.class);
    List<UserAuthorityDto> authorities = authorityDto.getAuthorities().stream()
            .collect(Collectors.toList());

    return CompletableFuture.completedFuture(authorities);
}

方案2:添加请求重试机制

针对第三方服务偶尔返回空响应的情况,添加重试逻辑(这里手动实现简单重试,也可使用Spring Retry):

public CompletableFuture<List<UserAuthorityDto>> getAuthorities(String username) throws Exception {
    int maxRetry = 3;
    Exception lastException = null;

    for (int i = 0; i < maxRetry; i++) {
        try {
            SimpleHttpRequest request = SimpleRequestBuilder.get(url)
                    .addParameter("username", username)
                    .addHeader("Content-Type", "application/json")
                    .addHeader("Accept", "*/*")
                    .build();

            Future<SimpleHttpResponse> future = httpAsyncClient.execute(request, null);
            SimpleHttpResponse response = future.get();

            if (response.getCode() != 200 || StringUtils.isEmpty(response.getBody().getBodyText())) {
                throw new RuntimeException("请求第三方服务失败");
            }

            ObjectMapper objectMapper = new ObjectMapper();
            UserAuthorityDto authorityDto = objectMapper.readValue(response.getBody().getBodyText(), UserAuthorityDto.class);
            return CompletableFuture.completedFuture(authorityDto.getAuthorities());
        } catch (Exception e) {
            lastException = e;
            Thread.sleep(100); // 重试间隔100ms
        }
    }
    throw lastException;
}

方案3:排查第三方服务的并发限制

联系第三方服务提供商,确认是否存在同一用户/客户端的并发请求限制或会话绑定规则。如果是服务端限制了同一会话的并发请求,可以尝试在后端调用时为每个请求添加唯一标识头(如X-Request-ID),避免被识别为重复请求。

方案4:缓存权限信息

如果权限信息不会频繁变动,添加本地缓存或分布式缓存(如Redis),减少第三方服务调用次数:

@Cacheable(value = "userAuthorities", key = "#username", expireAfterWrite = 3600)
public List<UserAuthorityDto> getAuthorities(String username) throws Exception {
    // 原第三方服务调用逻辑
}

注意:需配置Spring Cache,并根据权限更新频率设置缓存过期时间。

为什么不推荐synchronized?

synchronized会将并发请求强制转为串行执行,虽然能绕过第三方服务的并发限制,但会严重降低后端接口吞吐量,高并发场景下会导致请求排队、响应延迟激增,不符合高可用系统的设计原则。

内容的提问来源于stack exchange,提问作者Aysu Vural

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 05:11:38