Spring Boot 請(qǐng)求處理超大JSON數(shù)據(jù)三種方式性能對(duì)比
環(huán)境:SpringBoot3.4.2
1. 簡(jiǎn)介
在 Web 項(xiàng)目開(kāi)發(fā)中,超大 JSON 數(shù)據(jù)請(qǐng)求的情況愈發(fā)常見(jiàn),比如物聯(lián)網(wǎng)設(shè)備批量上傳海量監(jiān)測(cè)數(shù)據(jù)、大數(shù)據(jù)分析系統(tǒng)接收大規(guī)模用戶行為信息等場(chǎng)景。
超大 JSON 數(shù)據(jù)不僅數(shù)據(jù)量龐大,可能達(dá)到幾十兆甚至上百兆,而且內(nèi)部結(jié)構(gòu)復(fù)雜,嵌套層級(jí)深、字段繁多。若處理不當(dāng),會(huì)引發(fā)諸多問(wèn)題,如內(nèi)存溢出,導(dǎo)致應(yīng)用崩潰;數(shù)據(jù)解析耗時(shí)過(guò)長(zhǎng),降低系統(tǒng)響應(yīng)速度,影響用戶體驗(yàn);還可能因數(shù)據(jù)傳輸不穩(wěn)定,造成數(shù)據(jù)丟失或損壞。
因此,如何讓 Spring Boot 應(yīng)用高效、穩(wěn)定地處理超大 JSON 數(shù)據(jù)請(qǐng)求,成為保障系統(tǒng)性能與可靠性的關(guān)鍵。
在本篇文章中,我們將運(yùn)用三種不同的方法,對(duì)處理超大 JSON 數(shù)據(jù)的性能展開(kāi)對(duì)比分析。
2.實(shí)戰(zhàn)案例
準(zhǔn)備數(shù)據(jù)
public record User(Long id, String name, Integer age, String sex, String email, String address) {
}準(zhǔn)備了50W條json數(shù)據(jù)
圖片
接下來(lái)的測(cè)試都是基于此數(shù)據(jù)進(jìn)行測(cè)試。
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
}這是一個(gè)普通 POST 請(qǐng)求處理方法。使用 @RequestBody 接收客戶端傳來(lái)的 List<User> 類型 JSON 數(shù)據(jù)。這種方式在數(shù)據(jù)量大時(shí)可能因同步處理和內(nèi)存占用導(dǎo)致性能瓶頸。
如下,通過(guò)JConsole查看,在處理整個(gè)請(qǐng)求過(guò)程中內(nèi)存的變化情況:
圖片
只要進(jìn)入到該方法中內(nèi)存就已經(jīng)暴增到最高點(diǎn)。
2.2 使用流式解析
使用 Jackson 的 JsonParser 逐塊讀取數(shù)據(jù),不加載整個(gè) JSON 到內(nèi)存。
@PostMapping("/stream")
public ResponseEntity<String> stream(InputStream inputStream) throws IOException {
  ObjectMapper mapper = new ObjectMapper();
  try (JsonParser parser = mapper.getFactory().createParser(inputStream)) {
    // 遍歷 JSON 結(jié)構(gòu)
    JsonToken token;
    while ((token = parser.nextToken()) != null) {
      if (token == JsonToken.START_ARRAY) {
        while (parser.nextToken() != JsonToken.END_ARRAY) {
          // 逐條處理數(shù)組中的對(duì)象
          User item = parser.readValueAs(User.class) ;
          processItem(item);
        }
      }
    }
  }
  try {
    TimeUnit.MILLISECONDS.sleep(2000) ;
  } catch (InterruptedException e) {}
  return ResponseEntity.ok("stream success");
}通過(guò)流式處理超大 JSON 數(shù)據(jù)。利用 InputStream 讀取數(shù)據(jù),借助 JsonParser 逐個(gè)解析 JSON 令牌。當(dāng)遇到數(shù)組起始時(shí),逐條讀取并處理 User 對(duì)象,避免一次性加載全部數(shù)據(jù)到內(nèi)存,有效降低內(nèi)存占用。
如下,通過(guò)JConsole查看,在處理整個(gè)請(qǐng)求過(guò)程中內(nèi)存的變化情況:
圖片
整體內(nèi)存變化非常的小。當(dāng)我們沒(méi)有進(jìn)入while循環(huán)處理的時(shí)候,內(nèi)存基本是沒(méi)有變化的,只有再真正處理數(shù)據(jù)的時(shí)候,內(nèi)存才會(huì)遞增。
2.3 使用響應(yīng)式WebFlux
使用 Spring WebFlux 實(shí)現(xiàn)非阻塞流式處理。
@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)) ;
}訪問(wèn)該接口,最終內(nèi)存情況如下:
圖片
與上面的InputStream處理方式一樣,只要沒(méi)有開(kāi)始處理數(shù)據(jù),內(nèi)存基本不會(huì)變化,只有開(kāi)始處理后內(nèi)存才開(kāi)始遞增。
















 
 
 







 
 
 
 