內(nèi)存優(yōu)化神器!Spring StreamingResponseBody 三大實(shí)戰(zhàn)技巧,告別 OOM!
在當(dāng)前高并發(fā)、大數(shù)據(jù)量的業(yè)務(wù)場(chǎng)景下,傳統(tǒng)的同步響應(yīng)模式往往會(huì)因內(nèi)存占用過(guò)高、響應(yīng)延遲大等問(wèn)題,成為系統(tǒng)性能的瓶頸。Spring Boot 3.4 提供的 StreamingResponseBody 機(jī)制,可以實(shí)現(xiàn)流式數(shù)據(jù)傳輸,使數(shù)據(jù)在生成的同時(shí)直接發(fā)送至客戶端,從而避免內(nèi)存中存儲(chǔ)完整響應(yīng)的需求。
無(wú)論是大文件下載、實(shí)時(shí)日志推送,還是動(dòng)態(tài)生成 CSV 數(shù)據(jù)并導(dǎo)出,StreamingResponseBody都能以極低的資源消耗實(shí)現(xiàn)高效數(shù)據(jù)傳輸。本文將通過(guò)以下三大典型場(chǎng)景,深入探討 StreamingResponseBody 的應(yīng)用方式:
- 大文件下載流式讀取文件內(nèi)容,避免 OOM 現(xiàn)象;
- 實(shí)時(shí)數(shù)據(jù)推送實(shí)現(xiàn)日志流、股票數(shù)據(jù)等動(dòng)態(tài)內(nèi)容推送;
- 動(dòng)態(tài) CSV 生成與導(dǎo)出高效分頁(yè)查詢數(shù)據(jù)庫(kù),避免字符串拼接導(dǎo)致的內(nèi)存開銷。
大文件下載:秒級(jí)傳輸,避免內(nèi)存溢出
在傳統(tǒng)的文件下載方式中,文件內(nèi)容通常會(huì)被完整加載到內(nèi)存中再返回給客戶端,對(duì)于大文件來(lái)說(shuō),這很容易導(dǎo)致內(nèi)存溢出。使用 StreamingResponseBody,可以邊讀取文件邊傳輸數(shù)據(jù),確保內(nèi)存占用始終處于合理水平。
代碼示例:
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);
}
}
關(guān)鍵點(diǎn):
- 流式讀取通過(guò) InputStream 逐塊讀取文件內(nèi)容,而非一次性加載到內(nèi)存;
- 實(shí)時(shí)寫出使用 flush() 確保數(shù)據(jù)立即發(fā)送,減少緩沖區(qū)積壓;
- 避免超時(shí)建議在 application.yml 配置 spring.mvc.async.request-timeout=-1 以防止長(zhǎng)時(shí)間下載失敗。
實(shí)時(shí)數(shù)據(jù)推送:高效流式傳輸日志
對(duì)于日志監(jiān)控、股票行情等實(shí)時(shí)更新的數(shù)據(jù),傳統(tǒng)方案往往需要輪詢獲取數(shù)據(jù),帶來(lái)額外的網(wǎng)絡(luò)開銷。使用 StreamingResponseBody,可以建立長(zhǎng)連接,讓服務(wù)器主動(dòng)推送最新數(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);
}
}
關(guān)鍵點(diǎn):
- 實(shí)時(shí)輸出調(diào)用 flush() 確保數(shù)據(jù)立即推送;
- 模擬動(dòng)態(tài)數(shù)據(jù)使用 Thread.sleep() 模擬數(shù)據(jù)產(chǎn)生的時(shí)間間隔;
- 客戶端測(cè)試可使用 curl http://localhost:8080/stream/logs 觀察數(shù)據(jù)流式推送的效果。
動(dòng)態(tài)生成 CSV 并下載
在導(dǎo)出大規(guī)模數(shù)據(jù)(如用戶數(shù)據(jù)、訂單數(shù)據(jù))時(shí),傳統(tǒng)方式通常先將所有數(shù)據(jù)拼接成字符串再寫入文件,這容易造成內(nèi)存溢出。通過(guò) StreamingResponseBody,我們可以實(shí)現(xiàn) 邊查詢、邊寫入、邊下載 的流式導(dǎo)出。
代碼示例:
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);
}
}
關(guān)鍵點(diǎn):
- 分頁(yè)查詢每次僅查詢一部分?jǐn)?shù)據(jù),避免一次性加載過(guò)多數(shù)據(jù);
- 邊查詢邊寫入防止字符串拼接導(dǎo)致的內(nèi)存溢出。
總結(jié)
StreamingResponseBody 提供了一種高效的流式數(shù)據(jù)傳輸方式,在大文件下載、實(shí)時(shí)推送、動(dòng)態(tài)導(dǎo)出等場(chǎng)景下能夠顯著降低內(nèi)存占用,提高系統(tǒng)吞吐量,讓你的應(yīng)用更具彈性與穩(wěn)定性!