【文件上传】记录一次大附件上传的处理方案

大附件上传处理方案

适用场景:表单或接口上传大附件时出现 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 用对象存储的原生分片上传,前端直传,后端签发凭证
相关推荐
溪语流沙1 小时前
【每天一个CSS | Day21】落地页Hero动画的分镜表
前端·css
neoooo1 小时前
🚀Claude Code 小技巧(一):一键进入危险模式
前端·人工智能·后端
bmxy小明同学3 小时前
2026-10-11-幻觉治理
服务器·前端·javascript
Coder_张三3 小时前
iOS 26 适配
前端·ios·swift
m0_587383003 小时前
无人自助拼豆系统落地开发实战指南 核心逻辑实现与部署经验分享
前端·javascript·vue.js·人工智能·经验分享·需求分析
zhaiyy3 小时前
HTML Popover API 实践
前端·javascript·html
牧艺4 小时前
SPA 也能像原生 App:View Transitions API 从原理到可上线实战
前端·javascript·css
Dovis(誓平步青云)4 小时前
现场问题先关闭还是先验收,建筑任务怎样处理依赖
前端·javascript·数据库·mysql·音视频
用户84298142418104 小时前
压缩 ≠ 加密:前端代码保护的三层实践与踩坑记录
前端·javascript