Spring Boot 做 RAG 文档上传:1GB 文件会不会打爆内存?

Spring Boot 做 RAG 文档上传:1GB 文件会不会打爆内存?

做 RAG 系统时,文档上传很容易被低估。

普通系统里,上传文件可能只是保存附件。但在 RAG 里,上传只是第一步,后面通常还有:

bash 复制代码
上传文档 -> 保存文件 -> 解析文本 -> 文本分片 -> 生成 embedding -> 写入向量库

如果用户上传一个 1GB 的 PDF 或 Word 文档,问题就变得很现实:

  1. Spring Boot 会不会先把 1GB 文件全部读进内存?
  2. 如何配置拒绝策略?

1. 先说结论

Spring Boot 已经提供了 multipart 上传大小限制。

常见配置如下:

bash 复制代码
spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 100MB

这两个是 Spring Boot 官方参数。

max-file-size 限制单个文件大小。

max-request-size 限制整个 multipart 请求大小,包括文件和普通表单字段。

重点是:Spring Boot 不会为了判断 1GB 文件是否超限,就先把 1GB 全部读进 JVM 堆内存。

在常规 Spring MVC 上传里,文件大小限制会在 multipart 解析阶段生效。很多情况下,Controller 方法还没进入,请求就已经因为超限失败了。

例如一个普通上传接口:

bash 复制代码
@PostMapping(value = "/documents/upload",
        consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void upload(@RequestPart("file") MultipartFile file) {
    documentService.upload(file);
}

这里的 MultipartFile 不等于文件内容已经完整在内存里。它只是业务代码访问上传文件的入口。

真正影响内存的是后面怎么用它。

不推荐:

bash 复制代码
byte[] bytes = file.getBytes();

推荐:

bash 复制代码
try (InputStream inputStream = file.getInputStream()) {
    fileStorage.save(inputStream, file.getOriginalFilename(), file.getSize());
}

简单说:

bash 复制代码
Spring Boot 能限制上传大小。
但业务代码自己全量读取文件,仍然会造成内存压力。

2. Spring Boot 上传文件的大致流程

文件上传一般是 multipart/form-data 请求。

请求头里通常会带上 Content-Length

bash 复制代码
POST /documents/upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
Content-Length: 1073741824

这个 Content-Length 表示请求体大小。服务端在读取完整文件之前,就能先知道这次请求大概有多大。

整体流程可以简单理解为:

bash 复制代码
客户端上传文件
        |
        v
Servlet 容器接收请求
        |
        v
Spring Boot / Servlet 解析 multipart
        |
        v
检查 max-file-size / max-request-size
        |
        v
没超限:生成 MultipartFile 给 Controller
超限:抛出上传大小异常

所以它不是:

bash 复制代码
先完整读入内存 -> 再判断大小

而是:

bash 复制代码
解析过程中检查限制
文件内容通过缓冲区和临时文件处理
业务代码拿到 MultipartFile 后再决定怎么读取

如果希望返回更友好的错误信息,可以统一处理上传超限异常:

bash 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public Result<Void> handleUploadSizeExceeded() {
        return Result.fail("上传文件大小超过限制");
    }
}

3. 真正容易出问题的地方

Spring Boot 的 multipart 配置解决的是上传入口问题。

但它不会替业务代码兜底。

最常见的问题是全量读文件:

bash 复制代码
byte[] bytes = file.getBytes();

或者:

bash 复制代码
byte[] bytes = inputStream.readAllBytes();

这些代码会把文件完整读进 JVM 堆内存。

如果文件是 50MB,并发 10 个请求,仅这一步就可能带来几百 MB 的内存压力。

RAG 系统还要注意另一个点:上传阶段和解析阶段不是一回事。

上传阶段可以是流式的:

bash 复制代码
try (InputStream inputStream = file.getInputStream()) {
    fileStorage.save(inputStream, file.getOriginalFilename(), file.getSize());
}

但后续解析阶段可能又全量读了文件:

bash 复制代码
try (InputStream inputStream = fileStorage.openStream(fileUrl)) {
    byte[] bytes = inputStream.readAllBytes();
    ParseResult result = parser.parse(bytes);
}

这种情况下,上传接口本身可能没问题,但解析任务仍然可能造成内存波动。

分片阶段也一样。

很多代码会先拿到完整文本:

bash 复制代码
String text = parser.extractText(inputStream);
List<DocumentChunk> chunks = chunker.split(text);

如果文档很大,完整文本和分片列表都会占用内存。

所以 RAG 文件链路不能只看上传接口,还要看:

bash 复制代码
上传是否流式
解析是否全量读
分片是否一次性生成
embedding 是否批量过大
向量库写入是否有限流

4. RAG 文件上传的建议

第一,后端必须配置上传大小限制。

bash 复制代码
spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 100MB

前端可以提示文件大小,但真正的限制一定要放在后端。

第二,上传阶段优先使用流。

推荐:

bash 复制代码
try (InputStream inputStream = file.getInputStream()) {
    fileStorage.save(inputStream, file.getOriginalFilename(), file.getSize());
}

谨慎使用:

bash 复制代码
byte[] bytes = file.getBytes();

第三,上传和解析最好解耦。

上传接口只做:

bash 复制代码
校验文件 -> 保存文件 -> 创建文档记录 -> 提交异步任务

后续任务再做:

bash 复制代码
解析 -> 分片 -> embedding -> 写入向量库

第四,上传并发要单独控制。

max-file-size 只限制单个文件大小,max-request-size 只限制单个请求大小,它们不限制同时有多少人在上传。

RAG 系统里,并发上传会带来临时文件、磁盘 IO、解析任务、embedding 请求和向量库写入压力。

第五,解析和分片阶段也要控内存。

可以考虑:

  1. 限制单个文档最大大小。
  2. 限制解析文本最大长度。
  3. 分片分批处理。
  4. embedding 分批提交。
  5. 解析任务设置并发上限。

总结一下:

bash 复制代码
1.
Spring Boot 不会把超限文件全部加载到内存再拒绝,而是有完善的早期拒绝机制

2.
Content-Length 请求头让服务器在读取请求体之前就知道请求大小

3.
Tomcat 在解析请求头时检查 Content-Length,超过 maxSwallowSize(默认 2MB)直接拒绝,不读取请求体

4.
Spring Boot 在解析 multipart 请求时,边读边检查文件大小,超过 max-file-size 立即停止并抛出异常

5.
即使文件没超限,Spring Boot 也不会把整个文件加载到内存,而是边读边写到临时文件,内存占用只有一个小的缓冲区(8KB)
相关推荐
蝎子莱莱爱打怪6 小时前
我花两年业余时间做了个IM系统,然后呢😂??
后端·flutter·面试
叫我少年6 小时前
.NET 11 来了:Kestrel 提速 40%,还有这些你可能不知道的变化
后端
用户2279584482876 小时前
医生问“现在还在吃吗”:EHR 用药 RAG 先看 effectivePeriod,别先信 note
后端
geovindu7 小时前
go: Read-Write Lock Pattern
开发语言·后端·设计模式·golang·读写锁模式
百珏7 小时前
AI 应用技术演进串讲大纲
人工智能·后端·架构
Bacon7 小时前
装上就回不去了:CodeGraph 让 AI 编程效率飙升 92%,它到底做了什么?
前端·人工智能·后端
Xiacqi17 小时前
Spring全局异常处理
java·后端
狗头大军之江苏分军7 小时前
Python 协程进化史:从 yield 到 async/await 的底层实现
前端·后端
浩风祭月8 小时前
把慢查询日志扔给 AI,从分析到修复只用了半小时:一份完整的实操手册
后端·ai编程