大附件上传处理方案
适用场景:表单或接口上传大附件时出现 413、500 或内存溢出,以及"文件太大怎么传"的方案选择。
一、概述
上传时附件大小会直接影响程序能否正常工作,涉及三层限制,任何一层不满足都会失败:
| 层 | 限制项 | 典型配置 | 超限时的表现 |
|---|---|---|---|
| 1. 反向代理 | 请求体大小 | nginx client_max_body_size |
413 Request Entity Too Large |
| 2. 开发框架(程序侧) | 单文件 / 单请求大小 | spring.servlet.multipart.* |
Server Internal Error |
| 3. JVM | 堆内存 | -Xms / -Xmx |
java.lang.OutOfMemoryError: Java heap space |
三层是依次生效的,所以排查时要看请求停在了哪一层(见第二章),而不是一上来就改配置。
二、报错分析
| 报错 | 抛出位置 | 请求走到哪一步 | 根因 |
|---|---|---|---|
413 Request Entity Too Large |
nginx | 还在 nginx,未转发到后端 | nginx 的请求体限制 |
Server Internal Error(常伴随 MaxUploadSizeExceededException、Maximum upload size exceeded) |
开发框架 | 到了框架,未进入业务代码 | 框架的 multipart 限制 |
java.lang.OutOfMemoryError: Java heap space |
业务程序 | 已进入业务代码 | 堆内存不足 |
判断信号:看日志里有没有出现业务代码的日志。只有 nginx access log 有记录 → 卡在第一层;框架异常栈 → 第二层;OOM 堆栈里能看到自己的类名 → 第三层。
2.1 413 Request Entity Too Large
一般是 nginx 抛出的,需要修改 nginx 配置后杀死 nginx 进程重启。
此时请求还在 nginx,尚未转发到后端,所以后端日志里什么都看不到。
2.2 Server Internal Error
一般是程序的大小限制,需要修改 multipart 参数(建议改在 nacos 里,便于统一管理且可动态生效)。
此时请求还在开发框架上,尚未到达我们的业务程序。
2.3 Java heap space
请求已经到达了业务程序,原因是堆内存不足。
典型触发点:把整个文件一次读进内存(byte[] / MultipartFile.getBytes())后再做转换、合并或入库。文件越大,内存峰值越高。
三、按层修改配置
3.1 nginx 配置
增加配置:
nginx
client_max_body_size 500M;
后面的值就是允许的最大请求体大小 ,按实际需要调整(也可写 0 表示不限制,但不建议,会失去防护作用)。
该配置可以加在三个地方,作用域不同,就近生效(内层覆盖外层):
① 加在 http 块:对整个 nginx 所有站点生效
nginx
http {
include mime.types;
default_type application/octet-stream;
client_max_body_size 500M;
# ... 其他配置
}
② 加在 server 块:对当前站点生效
nginx
server {
listen 80;
client_max_body_size 500M;
#server_name localhost;
# ... 其他配置
}
③ 加在 location 块:只对当前路径生效(推荐,影响面最小)
nginx
location /api/ {
proxy_set_header Host $host;
proxy_set_header developer ray;
proxy_pass http://127.0.0.1:9900/;
client_max_body_size 500M;
}
改完执行(示例):
bash
nginx -t # 先校验配置语法
nginx -s stop # 或 kill 掉 nginx 进程
nginx # 重新启动
3.2 程序侧配置(multipart)
可以修改微服务的 application.yml:
yaml
spring:
servlet:
multipart:
# max-file-size 是单个文件大小
# max-request-size 是设置总上传的数据大小
max-file-size: 50MB
max-request-size: 50MB
也可以在 nacos 中配置(如 nacos-config-dev.properties):
properties
# 单个文件大小
spring.servlet.multipart.max-file-size=200MB
# 总的请求大小
spring.servlet.multipart.max-request-size=200MB
nacos 的优先级比 application.yml 更高,所以两处都配了时以 nacos 为准;建议统一只在 nacos 里维护,避免"改了没生效"的困惑。
补充:如果请求还要经过网关(Spring Cloud Gateway),除了 multipart 参数,还需注意网关自身的请求体缓冲限制(如
spring.codec.max-in-memory-size),否则非 multipart 的大请求体可能在网关层就被拒。
3.3 JVM 堆内存配置
如果程序配置的堆内存太小,就会出现堆内存溢出,因此需要配置合适大小的内存,例如:
-Xms2g -Xmx2g
配置建议:
-Xms与-Xmx设成同一个值,避免运行期反复扩缩容;- 堆大小不超过物理内存的 50%~70%,要给其他进程和堆外内存留出空间;
- 通过启动脚本或容器环境变量(如
JAVA_OPTS)设置,注意与容器内存上限匹配,否则会被 OOM Killer 杀掉。
调大堆内存只是"兜底",真正解决大文件问题的手段是分段上传:让单次请求体始终很小,内存峰值自然可控。
四、分段上传
4.1 什么时候用分段上传
满足以下任一条,就应该用分段上传,而不是继续调大限制:
| 触发条件 | 说明 |
|---|---|
| 单文件超过几十 MB(如 > 50MB) | 继续调大 nginx / multipart 只是把风险推给内存 |
| 需要上传进度、暂停/继续、断点续传 | 只有分片才有"进度"概念(XMLHttpRequest.upload.onprogress 也可,但无法续传) |
| 网络不稳定(移动端、外网) | 单片失败只需重传该片,不必整个文件重来 |
| 需要秒传(相同文件不重复上传) | 按文件指纹判断服务端是否已存在 |
| 上传到对象存储 | OSS / MinIO 原生支持 multipart,方式是"前端直传 + 后端签名" |
4.2 整体流程
存储(本地/对象存储) 应用服务 前端 存储(本地/对象存储) 应用服务 前端 #mermaid-svg-nztiQAyIAFR5haS4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nztiQAyIAFR5haS4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nztiQAyIAFR5haS4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nztiQAyIAFR5haS4 .error-icon{fill:#552222;}#mermaid-svg-nztiQAyIAFR5haS4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nztiQAyIAFR5haS4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nztiQAyIAFR5haS4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nztiQAyIAFR5haS4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nztiQAyIAFR5haS4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nztiQAyIAFR5haS4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nztiQAyIAFR5haS4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nztiQAyIAFR5haS4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nztiQAyIAFR5haS4 .marker.cross{stroke:#333333;}#mermaid-svg-nztiQAyIAFR5haS4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nztiQAyIAFR5haS4 p{margin:0;}#mermaid-svg-nztiQAyIAFR5haS4 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nztiQAyIAFR5haS4 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-nztiQAyIAFR5haS4 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nztiQAyIAFR5haS4 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-nztiQAyIAFR5haS4 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-nztiQAyIAFR5haS4 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-nztiQAyIAFR5haS4 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-nztiQAyIAFR5haS4 .sequenceNumber{fill:white;}#mermaid-svg-nztiQAyIAFR5haS4 #sequencenumber{fill:#333;}#mermaid-svg-nztiQAyIAFR5haS4 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-nztiQAyIAFR5haS4 .messageText{fill:#333;stroke:none;}#mermaid-svg-nztiQAyIAFR5haS4 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nztiQAyIAFR5haS4 .labelText,#mermaid-svg-nztiQAyIAFR5haS4 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-nztiQAyIAFR5haS4 .loopText,#mermaid-svg-nztiQAyIAFR5haS4 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-nztiQAyIAFR5haS4 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-nztiQAyIAFR5haS4 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-nztiQAyIAFR5haS4 .noteText,#mermaid-svg-nztiQAyIAFR5haS4 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-nztiQAyIAFR5haS4 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nztiQAyIAFR5haS4 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nztiQAyIAFR5haS4 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-nztiQAyIAFR5haS4 .actorPopupMenu{position:absolute;}#mermaid-svg-nztiQAyIAFR5haS4 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-nztiQAyIAFR5haS4 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-nztiQAyIAFR5haS4 .actor-man circle,#mermaid-svg-nztiQAyIAFR5haS4 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-nztiQAyIAFR5haS4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 命中"秒传"时直接结束 loop每个未上传分片(并发 3 个左右) 1. init(fileKey, fileName, fileSize, total)1uploadId + 已上传分片列表(用于续传)22. POST chunk(uploadId, index, blob)3落临时文件 / 上传分片4该片成功53. merge(uploadId, total)6按序号流式合并7文件 id / 下载地址8
三个接口各司其职:
| 接口 | 作用 |
|---|---|
init |
建上传任务,返回 uploadId;若服务端已有完整文件,直接返回"秒传成功";返回已上传分片便于续传 |
chunk |
接收单个分片,落临时位置,记录分片序号;每次请求体只有分片大小 |
merge |
按序号合并成完整文件,落库并清理临时分片 |
4.3 前端处理(简单代码)
js
import axios from 'axios';
// 分片大小:5MB(与后端约定一致即可)
const CHUNK_SIZE = 5 * 1024 * 1024;
// 并发数:太大反而容易被限流、也占带宽
const MAX_CONCURRENT = 3;
// 接口前缀(按实际网关路由替换)
const API = '/api-demo/upload';
/** 1) 生成文件指纹:用于秒传与断点续传 */
function getFileKey(file) {
return `${file.name}_${file.size}_${file.lastModified}`;
}
/** 2) 切片:把大文件切成若干个 Blob */
function createChunks(file, chunkSize = CHUNK_SIZE) {
const chunks = [];
let start = 0;
while (start < file.size) {
chunks.push(file.slice(start, start + chunkSize));
start += chunkSize;
}
return chunks;
}
/**
* 3) 分段上传主流程
* @param file 文件对象
* @param onProgress 进度回调,参数为 0~100
*/
async function uploadByChunks(file, onProgress) {
const fileKey = getFileKey(file);
const chunks = createChunks(file);
// 3.1 初始化:拿 uploadId 与已上传分片(续传用)
const initRes = await axios.post(`${API}/init`, {
fileKey,
fileName: file.name,
fileSize: file.size,
total: chunks.length
});
const { uploadId, uploaded = [], instant } = initRes.data;
// 命中秒传:服务端已存在同一文件,直接结束
if (instant) {
onProgress && onProgress(100);
return initRes.data;
}
// 3.2 过滤掉已上传的分片(断点续传)
const pending = chunks
.map((blob, index) => ({ blob, index }))
.filter(item => uploaded.indexOf(item.index) === -1);
// 3.3 分批并发上传
let finished = 0;
for (let i = 0; i < pending.length; i += MAX_CONCURRENT) {
const group = pending.slice(i, i + MAX_CONCURRENT);
await Promise.all(group.map(async item => {
const form = new FormData();
form.append('uploadId', uploadId);
form.append('index', item.index);
form.append('chunk', item.blob);
await axios.post(`${API}/chunk`, form, {
headers: { 'Content-Type': 'multipart/form-data' }
});
finished++;
onProgress && onProgress(Math.round(finished / pending.length * 100));
}));
}
// 3.4 合并
const mergeRes = await axios.post(`${API}/merge`, {
uploadId,
fileKey,
fileName: file.name,
total: chunks.length
});
return mergeRes.data;
}
export { uploadByChunks, getFileKey, createChunks };
使用:
js
// 组件里的用法
const file = event.target.files[0];
uploadByChunks(file, percent => {
this.percent = percent; // 直接拿进度更新进度条
}).then(res => {
this.fileId = res.fileId; // 合并成功后拿到文件 id,随表单一起提交
}).catch(err => {
console.error('上传失败', err);
});
4.4 服务端要点
| 要点 | 做法 |
|---|---|
| 分片落盘 | chunk 接口把分片写入临时文件/对象存储分片,不要整体读进内存 ;用一个 uploadId 目录存放所有片 |
| 合并要流式 | 按序号依次追加(FileChannel.transferTo、SequenceInputStream 或对象存储的 complete 接口),避免"读全部片 → 拼成 byte\[\]" |
| 顺序与完整性 | 合并前校验分片数量/序号齐全;建议在 init 时顺带返回服务端计算的文件摘要,合并后比对 |
| 临时文件清理 | 定时清理超过 N 小时未合并的 uploadId 目录,防止磁盘被占满 |
| 并发与限流 | 单片大小 × 并发数要小于 nginx / multipart 的阈值;并发数建议 ≤ 4 |
4.5 一个容易被忽略的好处
分段后单次请求体只有 5MB 左右,于是:
- nginx 的
client_max_body_size只需放行 ~10MB(比放行 500MB 安全得多); - multipart 的
max-request-size也可以保持较小,不会因为一个接口放开整个服务的上传限制; - 堆内存峰值不再随文件大小线性增长。
也就是说,分段上传是"不用调大配置也能传大文件"的正解;只有当分段也做不了(比如第三方系统只能整包传)时,才回到第三章调配置。
若已接入对象存储(OSS / MinIO),优先用它的分片上传接口并让前端直传(后端只签发临时凭证),请求根本不经过应用服务与网关,是最省资源的方案。
五、压缩上传
5.1 什么时候能压、什么时候不能压
| 文件类型 | 能否压缩 | 说明 |
|---|---|---|
| 图片(jpg / png / webp) | ✅ 强烈建议 | 手机拍的图动辄 5~15MB,而展示只需要 1~2MB;缩放 + 重编码通常能减到原文的 10%~30% |
| 文本类(txt / csv / json / log / xml) | ✅ 建议 gzip | 压缩率高(常 70%~90%) |
| Office 文档(docx / xlsx / pptx) | ❌ 不要 | 本身就是 zip 包,二次压缩几乎无收益 |
| 已压缩格式(zip / rar / jpg / png / mp4 / pdf) | ❌ 不要 | 再 gzip 只浪费 CPU,体积几乎不变 |
| 视频 / 镜像 / 超大压缩包 | ❌ 不要 | 前端压缩无意义且极慢,应直接走分段上传 |
判断口诀:"图片压分辨率,文本压 gzip,已压缩的不再压,视频只分段。"
5.2 图片压缩(简单代码)
js
/**
* 图片压缩:按最长边缩放后重编码
* @param file 原始文件
* @param maxSize 最长边上限(像素),默认 1920
* @param quality 编码质量 0~1,默认 0.7(仅 jpeg/webp 有效)
* @returns Promise<File>
*/
function compressImage(file, maxSize = 1920, quality = 0.7) {
return new Promise((resolve, reject) => {
// 非图片、或本来就很小,不处理
if (!/^image\/(jpeg|png|webp)$/.test(file.type) || file.size < 1024 * 1024) {
return resolve(file);
}
const img = new Image();
const url = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(url);
// 1) 等比缩放到最长边不超过 maxSize
let { width, height } = img;
if (width > maxSize || height > maxSize) {
const ratio = maxSize / Math.max(width, height);
width = Math.round(width * ratio);
height = Math.round(height * ratio);
}
// 2) 画到 canvas 上重编码
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(img, 0, 0, width, height);
// png 想保留透明通道就输出 png;否则输出 jpeg 体积更小
const keepPng = file.type === 'image/png';
const outType = keepPng ? 'image/png' : 'image/jpeg';
canvas.toBlob(blob => {
if (!blob) {
return reject(new Error('图片压缩失败'));
}
// 压完反而更大(小图常见),就用原文件
if (blob.size >= file.size) {
return resolve(file);
}
resolve(new File([blob], file.name, {
type: outType,
lastModified: file.lastModified
}));
}, outType, quality);
};
img.onerror = () => {
URL.revokeObjectURL(url);
reject(new Error('图片解码失败,可能不是有效图片'));
};
img.src = url;
});
}
使用(压缩后再走普通上传或分段上传):
js
const raw = event.target.files[0];
const file = await compressImage(raw, 1920, 0.7); // 一般能减到 10%~30%
await uploadByChunks(file, percent => { this.percent = percent; });
两点注意:
- 保留透明:png 输出 png;若不介意丢透明,输出 jpeg 体积更小。
- 需要原图的场景不要压(电子发票、证件照、扫描件),否则清晰度不达标还得重传。
5.3 文本类压缩
js
/** 用浏览器原生 API 做 gzip(需后端能解压,如 Content-Encoding: gzip 或自行解压) */
async function gzipFile(file) {
if (!/^text\/|\.(txt|csv|json|log|xml)$/i.test(file.type || file.name)) {
return file;
}
const stream = file.stream().pipeThrough(new CompressionStream('gzip'));
const blob = await new Response(stream).blob();
return new File([blob], file.name + '.gz', { type: 'application/gzip' });
}
CompressionStream需要较新的浏览器;不支持时直接不压缩即可。压完记得告知后端是 gzip 内容。
六、处理决策
#mermaid-svg-R557vIUJlLuL00jN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-R557vIUJlLuL00jN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-R557vIUJlLuL00jN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-R557vIUJlLuL00jN .error-icon{fill:#552222;}#mermaid-svg-R557vIUJlLuL00jN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-R557vIUJlLuL00jN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-R557vIUJlLuL00jN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-R557vIUJlLuL00jN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-R557vIUJlLuL00jN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-R557vIUJlLuL00jN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-R557vIUJlLuL00jN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-R557vIUJlLuL00jN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-R557vIUJlLuL00jN .marker.cross{stroke:#333333;}#mermaid-svg-R557vIUJlLuL00jN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-R557vIUJlLuL00jN p{margin:0;}#mermaid-svg-R557vIUJlLuL00jN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-R557vIUJlLuL00jN .cluster-label text{fill:#333;}#mermaid-svg-R557vIUJlLuL00jN .cluster-label span{color:#333;}#mermaid-svg-R557vIUJlLuL00jN .cluster-label span p{background-color:transparent;}#mermaid-svg-R557vIUJlLuL00jN .label text,#mermaid-svg-R557vIUJlLuL00jN span{fill:#333;color:#333;}#mermaid-svg-R557vIUJlLuL00jN .node rect,#mermaid-svg-R557vIUJlLuL00jN .node circle,#mermaid-svg-R557vIUJlLuL00jN .node ellipse,#mermaid-svg-R557vIUJlLuL00jN .node polygon,#mermaid-svg-R557vIUJlLuL00jN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-R557vIUJlLuL00jN .rough-node .label text,#mermaid-svg-R557vIUJlLuL00jN .node .label text,#mermaid-svg-R557vIUJlLuL00jN .image-shape .label,#mermaid-svg-R557vIUJlLuL00jN .icon-shape .label{text-anchor:middle;}#mermaid-svg-R557vIUJlLuL00jN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-R557vIUJlLuL00jN .rough-node .label,#mermaid-svg-R557vIUJlLuL00jN .node .label,#mermaid-svg-R557vIUJlLuL00jN .image-shape .label,#mermaid-svg-R557vIUJlLuL00jN .icon-shape .label{text-align:center;}#mermaid-svg-R557vIUJlLuL00jN .node.clickable{cursor:pointer;}#mermaid-svg-R557vIUJlLuL00jN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-R557vIUJlLuL00jN .arrowheadPath{fill:#333333;}#mermaid-svg-R557vIUJlLuL00jN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-R557vIUJlLuL00jN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-R557vIUJlLuL00jN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R557vIUJlLuL00jN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-R557vIUJlLuL00jN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R557vIUJlLuL00jN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-R557vIUJlLuL00jN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-R557vIUJlLuL00jN .cluster text{fill:#333;}#mermaid-svg-R557vIUJlLuL00jN .cluster span{color:#333;}#mermaid-svg-R557vIUJlLuL00jN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-R557vIUJlLuL00jN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-R557vIUJlLuL00jN rect.text{fill:none;stroke-width:0;}#mermaid-svg-R557vIUJlLuL00jN .icon-shape,#mermaid-svg-R557vIUJlLuL00jN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-R557vIUJlLuL00jN .icon-shape p,#mermaid-svg-R557vIUJlLuL00jN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-R557vIUJlLuL00jN .icon-shape .label rect,#mermaid-svg-R557vIUJlLuL00jN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-R557vIUJlLuL00jN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-R557vIUJlLuL00jN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-R557vIUJlLuL00jN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 图片、文本类
视频、压缩包、需原图
是
否
否
是
文件过大 / 上传失败
可以压缩吗?
前端压缩
跳过压缩
压缩后够小吗?
普通上传
分段上传(5MB/片)
还会报错吗?
按第三章调配置
nginx / multipart / JVM
对应的选择建议:
| 文件情况 | 压缩 | 分段 | 说明 |
|---|---|---|---|
| 图片 1~10MB | ✅ 必做 | 不必 | 压缩后通常只有原来的 10%~30% |
| 图片 > 10MB,或需保留原图 | 可选 | ✅ | 证件照、扫描件、发票类 |
| 文本 / CSV / 日志(≤ 50MB) | ✅ gzip | 不必 | 压缩率高 |
| Office 文档(≤ 50MB) | ❌ | 不必 | docx/xlsx 已是 zip |
| 视频 / 压缩包 / 镜像(> 100MB) | ❌ | ✅ 必须 | 前端压缩无意义 |
| 压缩 + 分段后仍报错 | --- | --- | 按第三章调三处配置 |
推荐顺序:先压缩 → 再分段 → 最后才调配置。 配置是兜底手段,不是第一选择。
七、速查表
| 现象 | 判断依据 | 处理 |
|---|---|---|
413 Request Entity Too Large |
只有 nginx 日志,后端无记录 | 3.1 改 client_max_body_size(建议加在 location),重启 nginx |
Server Internal Error / MaxUploadSizeExceededException |
框架异常栈,无业务日志 | 3.2 改 nacos 里的 spring.servlet.multipart.max-file-size / max-request-size |
java.lang.OutOfMemoryError: Java heap space |
OOM 堆栈里有业务类名 | 优先改为分段上传(第四章);必要时 3.3 调大 -Xms/-Xmx |
| 图片上传慢、体积大 | 手机原图常见 | 5.2 前端压缩 |
| 需要进度条 / 断点续传 / 秒传 | 需求层面要求 | 4.3 分段上传 |
| 上传到对象存储 | 已接入 OSS / MinIO | 用对象存储的原生分片上传,前端直传,后端签发凭证 |