內存優(yōu)化神器!Spring StreamingResponseBody 三大實戰(zhàn)技巧,告別 OOM!
在當前高并發(fā)、大數(shù)據(jù)量的業(yè)務場景下,傳統(tǒng)的同步響應模式往往會因內存占用過高、響應延遲大等問題,成為系統(tǒng)性能的瓶頸。Spring Boot 3.4 提供的 StreamingResponseBody 機制,可以實現(xiàn)流式數(shù)據(jù)傳輸,使數(shù)據(jù)在生成的同時直接發(fā)送至客戶端,從而避免內存中存儲完整響應的需求。
無論是大文件下載、實時日志推送,還是動態(tài)生成 CSV 數(shù)據(jù)并導出,StreamingResponseBody都能以極低的資源消耗實現(xiàn)高效數(shù)據(jù)傳輸。本文將通過以下三大典型場景,深入探討 StreamingResponseBody 的應用方式:
- 大文件下載流式讀取文件內容,避免 OOM 現(xiàn)象;
- 實時數(shù)據(jù)推送實現(xiàn)日志流、股票數(shù)據(jù)等動態(tài)內容推送;
- 動態(tài) CSV 生成與導出高效分頁查詢數(shù)據(jù)庫,避免字符串拼接導致的內存開銷。
大文件下載:秒級傳輸,避免內存溢出
在傳統(tǒng)的文件下載方式中,文件內容通常會被完整加載到內存中再返回給客戶端,對于大文件來說,這很容易導致內存溢出。使用 StreamingResponseBody,可以邊讀取文件邊傳輸數(shù)據(jù),確保內存占用始終處于合理水平。
代碼示例:
package com.icoderoad.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@RestController
@RequestMapping("/files")
public class FileDownloadController {
@Value("file:///d:/software/OllamaSetup.exe")
private Resource file;
@GetMapping("/download")
public ResponseEntity<StreamingResponseBody> downloadFile() throws Exception {
String fileName = file.getFilename();
StreamingResponseBody responseBody = outputStream -> {
try (InputStream inputStream = file.getInputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
outputStream.flush();
}
}
};
return ResponseEntity.ok()
.header("Content-Type", "application/octet-stream")
.header("Content-Disposition", "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8))
.body(responseBody);
}
}
關鍵點:
- 流式讀取通過 InputStream 逐塊讀取文件內容,而非一次性加載到內存;
- 實時寫出使用 flush() 確保數(shù)據(jù)立即發(fā)送,減少緩沖區(qū)積壓;
- 避免超時建議在 application.yml 配置 spring.mvc.async.request-timeout=-1 以防止長時間下載失敗。
實時數(shù)據(jù)推送:高效流式傳輸日志
對于日志監(jiān)控、股票行情等實時更新的數(shù)據(jù),傳統(tǒng)方案往往需要輪詢獲取數(shù)據(jù),帶來額外的網絡開銷。使用 StreamingResponseBody,可以建立長連接,讓服務器主動推送最新數(shù)據(jù)。
代碼示例:
package com.icoderoad.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Random;
@RestController
@RequestMapping("/stream")
public class RealTimeDataController {
@GetMapping("/logs")
public ResponseEntity<StreamingResponseBody> streamLogs() {
StreamingResponseBody responseBody = outputStream -> {
for (int i = 0; i < 20; i++) {
String log = "日志數(shù)據(jù) " + i + " - " + LocalDateTime.now() + "\n";
outputStream.write(log.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
Thread.sleep(new Random().nextInt(1000));
}
};
return ResponseEntity.ok()
.header("Content-Type", "text/plain;charset=utf-8")
.body(responseBody);
}
}
關鍵點:
- 實時輸出調用 flush() 確保數(shù)據(jù)立即推送;
- 模擬動態(tài)數(shù)據(jù)使用 Thread.sleep() 模擬數(shù)據(jù)產生的時間間隔;
- 客戶端測試可使用 curl http://localhost:8080/stream/logs 觀察數(shù)據(jù)流式推送的效果。
動態(tài)生成 CSV 并下載
在導出大規(guī)模數(shù)據(jù)(如用戶數(shù)據(jù)、訂單數(shù)據(jù))時,傳統(tǒng)方式通常先將所有數(shù)據(jù)拼接成字符串再寫入文件,這容易造成內存溢出。通過 StreamingResponseBody,我們可以實現(xiàn) 邊查詢、邊寫入、邊下載 的流式導出。
代碼示例:
package com.icoderoad.controller;
import com.icoderoad.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RestController
@RequestMapping("/export")
public class CsvDownloadController {
private final UserService userService;
public CsvDownloadController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users")
public ResponseEntity<StreamingResponseBody> exportCsv() {
StreamingResponseBody responseBody = outputStream -> {
outputStream.write("ID,Name,Email\n".getBytes());
for (int page = 0; page < 10; page++) {
List<User> users = userService.getUsers(page, 10);
for (User user : users) {
outputStream.write((user.id() + "," + user.name() + "," + user.email() + "\n").getBytes(StandardCharsets.UTF_8));
}
outputStream.flush();
}
};
return ResponseEntity.ok()
.header("Content-Type", "text/csv")
.header("Content-Disposition", "attachment; filename=\"users.csv\"")
.body(responseBody);
}
}
關鍵點:
- 分頁查詢每次僅查詢一部分數(shù)據(jù),避免一次性加載過多數(shù)據(jù);
- 邊查詢邊寫入防止字符串拼接導致的內存溢出。
總結
StreamingResponseBody 提供了一種高效的流式數(shù)據(jù)傳輸方式,在大文件下載、實時推送、動態(tài)導出等場景下能夠顯著降低內存占用,提高系統(tǒng)吞吐量,讓你的應用更具彈性與穩(wěn)定性!