Spring Boot 请求处理超大JSON数据 3种 方式性能对比
1. 简介
在 Web 项目开发中,超大 JSON 数据请求的情况愈发常见,比如物联网设备批量上传海量监测数据、大数据分析系统接收大规模用户行为信息等场景。
超大 JSON 数据不仅数据量庞大,可能达到几十兆甚至上百兆,而且内部结构复杂,嵌套层级深、字段繁多。若处理不当,会引发诸多问题,如内存溢出,导致应用崩溃;数据解析耗时过长,降低系统响应速度,影响用户体验;还可能因数据传输不稳定,造成数据丢失或损坏。
因此,如何让 Spring Boot 应用高效、稳定地处理超大 JSON 数据请求,成为保障系统性能与可靠性的关键。
运用三种不同的方法,对处理超大 JSON 数据的性能展开对比分析。
2.实战案例
准备数据
public record User(Long id, String name, Integer age, String sex, String email, String address) {
}
准备了50W条json数据
接下来的测试都是基于此数据进行测试。
2.1 普通处理方式
@PostMapping("/normal")
public ResponseEntity<String> normal(@RequestBody List<User> datas) {
datas.forEach(this::processItem) ;
try {
TimeUnit.MILLISECONDS.sleep(2000) ;
} catch (InterruptedException e) {}
return ResponseEntity.ok("normal success") ;
}
private void processItem(User item) {
// todo
}
这是一个普通 POST 请求处理方法。使用 @RequestBody 接收客户端传来的 List<User> 类型 JSON 数据。这种方式在数据量大时可能因同步处理和内存占用导致性能瓶颈。
如下,通过JConsole查看,在处理整个请求过程中内存的变化情况:
只要进入到该方法中内存就已经暴增到最高点。
2.2 使用流式解析
使用 Jackson 的 JsonParser 逐块读取数据,不加载整个 JSON 到内存。
@PostMapping("/stream")
public ResponseEntity<String> stream(InputStream inputStream) throws IOException {
ObjectMapper mapper = new ObjectMapper();
try (JsonParser parser = mapper.getFactory().createParser(inputStream)) {
// 遍历 JSON 结构
JsonToken token;
while ((token = parser.nextToken()) != null) {
if (token == JsonToken.START_ARRAY) {
while (parser.nextToken() != JsonToken.END_ARRAY) {
// 逐条处理数组中的对象
User item = parser.readValueAs(User.class) ;
processItem(item);
}
}
}
}
try {
TimeUnit.MILLISECONDS.sleep(2000) ;
} catch (InterruptedException e) {}
return ResponseEntity.ok("stream success");
}
通过流式处理超大 JSON 数据。利用 InputStream 读取数据,借助 JsonParser 逐个解析 JSON 令牌。当遇到数组起始时,逐条读取并处理 User 对象,避免一次性加载全部数据到内存,有效降低内存占用。
如下,通过JConsole查看,在处理整个请求过程中内存的变化情况:
整体内存变化非常的小。当我们没有进入while循环处理的时候,内存基本是没有变化的,只有再真正处理数据的时候,内存才会递增。
2.3 使用响应式WebFlux
使用 Spring WebFlux 实现非阻塞流式处理。
@PostMapping(value = "/reactive")
public Mono<String> fluxJson(@RequestBody Flux<User> items) {
return items.doOnNext(this::processItem)
.doOnComplete(() -> {
System.err.println("处理完成...") ;
})
.then(Mono.just("flux success"))
.delayElement(Duration.ofSeconds(2)) ;
}
访问该接口,最终内存情况如下:
与上面的InputStream处理方式一样,只要没有开始处理数据,内存基本不会变化,只有开始处理后内存才开始递增。