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);
问题根源分析
- 第三方服务的并发/会话限制:原第三方服务可能对同一客户端的并发请求有限制,或存在会话绑定逻辑——后端同时发起的两个请求被识别为同一会话的重复请求,直接返回空响应。而Postman、Angular直接调用时,请求上下文(如Cookie、请求头标识)与后端调用不同,避开了这个限制。
- HTTP客户端资源未复用:当前代码每次调用都新建并销毁
CloseableHttpAsyncClient,没有复用连接池。频繁创建TCP连接可能触发第三方服务的短连接限制,并发请求时第二个请求的连接未正常建立,导致空响应。 - 同步阻塞放大问题:虽然使用了异步客户端,但
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
相关产品推荐
相关产品推荐

