标题已经足够说明这是一道高频面试题大文件上传、断点续传、秒传。很多人简历里写了“熟悉文件上传”但面试官一旦追问到分片合并、断点续传的状态识别、并发上传的冲突处理立刻就乱了。本文不准备给一份“背题答案”而是把这个技术点从原理拆到实现再从实现拆到面试表达帮助你既能把代码写明白也能把面试聊明白。1. 先想清楚大文件上传到底难在哪先说一个判断大文件上传难的根本原因不是“上传动作本身”而是“HTTP 协议和浏览器的工作方式不适合传大文件”。我们先拆解几个基础事实一个普通 HTTP 请求从发起到服务端接收完响应超时、连接断开、服务器重启、浏览器被用户关掉任何一个环节出问题整个文件都要重新传。网络不是稳定的。一个 1GB 的文件在弱网环境下很容易传一半就断断掉以后全量重传的成本极高。浏览器有内存压力。如果前端一次性把整个文件 readAsArrayBuffer再 POST 给后端小文件问题不大上百 MB 甚至上 GB 的文件浏览器内存和网络栈都会吃紧严重的会导致页面崩溃。后端也有内存压力。如果后端接口直接接收 MultipartFile再一次性转存、合并JVM 堆内存很容易被大文件撑爆OOM 就是这样来的。所以大文件上传的工程问题本质上是三个如何降低单次传输的失败成本如何识别一个文件上传到哪个阶段如何避免大文件对浏览器和服务端内存造成冲击。答案就是分片上传、断点续传、秒传三个技术组合。2. 分片、断点续传、秒传先分清三个概念很多面试者会把这三个概念混在一起说其实它们是不同层面的东西面试官一听到你混淆印象分就会打折扣。2.1 分片上传把一个大文件按固定大小切成多个块Chunk前端逐个上传这些块后端接收全部块以后再按顺序合并成完整文件。分片的作用是降低单次传输的粒度让失败的影响范围变小同时可以利用并发上传多个分片来提升速度。2.2 断点续传断点续传建立在分片上传的基础上。核心思想是记录每个分片的上传状态上传中断后不需要重新上传整个文件只需要上传缺失的分片。关键点在于“记录状态”。这个状态可以存在前端本地 localStorage / IndexedDB也可以存在后端 Redis 或数据库更合理的是两者结合。2.3 秒传秒传的本质是“不需要真正传文件”。在用户选择文件后前端计算文件的唯一标识通常用 MD5生产环境建议用 SHA-1/SHA-256 或对超大文件做抽样哈希把这个标识发给后端。后端如果发现该标识的文件已经存在就直接返回上传完成的成功结果前端跳过上传过程。秒传不是“把文件传得很快”而是“一步都不用传”。三者关系可以这样理解概念解决的问题依赖关系分片上传单次传输粒度太大、失败成本高无断点续传传输中断后的恢复成本依赖分片 状态记录秒传相同文件重复上传的浪费依赖文件唯一标识 服务端存储比对面试时如果能把三个概念这样拆开就已经比大多数背书式回答强了。3. 分片上传的核心流程从选文件到合并完成下面结合一个标准的上传流程把整体链路讲清楚。3.1 整体流程用户选择文件。前端计算文件标识这里用 md5并读取文件大小、分片大小、分片总数。前端检查“秒传状态”把 md5 发给后端如果后端说“已存在”直接结束。如果不存在前端再检查“断点续传状态”把 md5 和已上传的分片序号发给后端后端返回缺失的分片序号。前端循环上传缺失分片。每个分片携带文件 md5、分片序号、总分片数等元数据。前端在所有分片上传完成后调用后端“合并接口”。后端验证分片完整性按序号合并文件更新文件状态。如果合并前发现某些分片缺失返回缺失列表让前端补传。3.2 分片大小怎么定分片大小没有绝对标准常见范围是 1MB 到 20MB。分片太小会导致请求数量太多加重服务端压力分片太大会失去“分片”的意义。一般推荐普通项目2MB 到 5MB。弱网场景1MB 到 2MB。局域网高速环境10MB 以上也不是问题。需要注意分片大小影响的不仅是上传速度还影响断点续传的恢复粒度。3.3 文件唯一标识的选择最直接的想法是计算整个文件的 MD5但大文件计算 MD5 有两个问题计算时间长消耗前端 CPU 和内存。工程上常用方案是抽样哈希取文件头部、中部、尾部的若干块数据再结合文件大小、修改时间计算一个相对可靠的标识。虽然理论上存在碰撞概率但对于非安全场景已经够用。如果项目对完整性校验要求高可以让前端计算每个分片的 MD5服务端合并前做一致性校验。4. 环境准备与前置条件本文的示例以后端 Spring Boot 前端原生 JavaScript 为基础。环境清单如下JDK 8 及以上Spring Boot 2.x / 3.x 均可示例中不依赖高版本特性Maven 3.xRedis用于记录分片上传状态本地磁盘或 MinIO用于最终文件存储支持 File.slice 的现代浏览器。如果不想引入 Redis也可以先用数据库表或内存 Map 做演示但生产环境必须用 Redis 或数据库这类持久化组件因为服务重启后内存数据会丢失。需要说明的是不同版本的 Spring Boot 在 API 细节上有差异本文重点展示通用思路版本号以你项目实际为准。5. 完整示例实现代码一步步走下面给出一个可运行的最小实现。目标是让你先跑通链路再理解关键点。5.1 前端分片上传代码前端用原生 JavaScript 实现不依赖框架。核心是 File.slice 方法。// 文件路径frontend/upload.js const CHUNK_SIZE 2 * 1024 * 1024; // 2MB async function uploadFile(file) { const fileMd5 await calculateFileMd5(file); const totalChunks Math.ceil(file.size / CHUNK_SIZE); // 1. 先检查文件在服务端是否已存在秒传 const checkResponse await fetch(/api/upload/check, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ fileMd5, fileName: file.name, fileSize: file.size }) }); const checkResult await checkResponse.json(); if (checkResult.exists) { console.log(秒传命中无需上传); return; } // 2. 获取未上传的分片序号 const uploadedChunks checkResult.uploadedChunks || []; // 3. 并发上传分片这里用 Promise.all 控制并发注意控制数量 const concurrency 3; const tasks []; for (let i 0; i totalChunks; i) { if (uploadedChunks.includes(i)) { continue; } const start i * CHUNK_SIZE; const end Math.min(file.size, start CHUNK_SIZE); const chunk file.slice(start, end); const formData new FormData(); formData.append(file, chunk); formData.append(fileMd5, fileMd5); formData.append(chunkIndex, i); formData.append(totalChunks, totalChunks); formData.append(fileName, file.name); const task fetch(/api/upload/chunk, { method: POST, body: formData }).then(res res.json()); tasks.push(task); if (tasks.length concurrency) { await Promise.all(tasks); tasks.length 0; } } if (tasks.length 0) { await Promise.all(tasks); } // 4. 通知服务端合并 const mergeResponse await fetch(/api/upload/merge, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ fileMd5, fileName: file.name, totalChunks }) }); const mergeResult await mergeResponse.json(); console.log(合并结果, mergeResult); } function calculateFileMd5(file) { return new Promise((resolve, reject) { const reader new FileReader(); reader.onload (e) { // 这里只做示意实际会使用 spark-md5 或 crypto.subtle 计算 // 由于 FileReader 读取全文件可能内存占用过大建议抽样计算 const buffer e.target.result; resolve(md5Hex(buffer)); }; reader.onerror reject; reader.readAsArrayBuffer(file); }); } function md5Hex(buffer) { // 实际替换为 spark-md5 / crypto.subtle 对 ArrayBuffer 的摘要计算 return mock-md5-value; }注意上面的 md5Hex 是占位函数实际项目中推荐使用 spark-md5 或 Web Crypto API。如果文件很大不要一次性读取整个文件计算 MD5建议抽样或分块读取。5.2 后端分片上传接口Spring Boot 接收分片将分片保存到临时目录并把上传状态记录到 Redis。// 文件路径src/main/java/com/example/upload/controller/UploadController.java RestController RequestMapping(/api/upload) public class UploadController { Resource private StringRedisTemplate stringRedisTemplate; private static final String UPLOAD_KET_PREFIX upload:chunks:; private static final String TEMP_FILE_PREFIX /tmp/upload/; /** * 校验文件是否已存在秒传以及已上传的分片列表断点续传 */ PostMapping(/check) public MapString, Object check(RequestBody MapString, String params) { String fileMd5 params.get(fileMd5); String fileName params.get(fileName); String fileSize params.get(fileSize); // 判断最终文件是否已存在 boolean exists fileExists(fileMd5, fileName, Long.parseLong(fileSize)); if (exists) { return Map.of(exists, true, uploadedChunks, List.of()); } // 从 Redis 读取已上传的分片序号 String redisKey UPLOAD_KET_PREFIX fileMd5; SetString members stringRedisTemplate.opsForSet().members(redisKey); ListInteger uploadedChunks members.stream() .map(Integer::parseInt) .sorted() .collect(Collectors.toList()); MapString, Object result new HashMap(); result.put(exists, false); result.put(uploadedChunks, uploadedChunks); return result; } /** * 接收单个分片 */ PostMapping(/chunk) public MapString, Object uploadChunk(MultipartFile file, String fileMd5, Integer chunkIndex, Integer totalChunks, String fileName) throws IOException { // 1. 保存分片到临时目录分片目录按 fileMd5 隔离 String chunkDir TEMP_FILE_PREFIX fileMd5; File dir new File(chunkDir); if (!dir.exists()) { dir.mkdirs(); } String chunkFileName chunkDir / chunkIndex .part; file.transferTo(new File(chunkFileName)); // 2. 记录已上传状态到 Redis String redisKey UPLOAD_KET_PREFIX fileMd5; stringRedisTemplate.opsForSet().add(redisKey, String.valueOf(chunkIndex)); MapString, Object result new HashMap(); result.put(success, true); result.put(chunkIndex, chunkIndex); return result; } /** * 合并分片 */ PostMapping(/merge) public MapString, Object merge(RequestBody MapString, String params) throws IOException { String fileMd5 params.get(fileMd5); String fileName params.get(fileName); int totalChunks Integer.parseInt(params.get(totalChunks)); String chunkDir TEMP_FILE_PREFIX fileMd5; String finalFilePath TEMP_FILE_PREFIX fileMd5 _ fileName; // 1. 合并前校验分片是否齐全 String redisKey UPLOAD_KET_PREFIX fileMd5; SetString uploaded stringRedisTemplate.opsForSet().members(redisKey); if (uploaded.size() ! totalChunks) { ListInteger missing new ArrayList(); for (int i 0; i totalChunks; i) { if (!uploaded.contains(String.valueOf(i))) { missing.add(i); } } return Map.of(success, false, missingChunks, missing); } // 2. 按序号顺序写入最终文件 try (FileOutputStream out new FileOutputStream(finalFilePath)) { for (int i 0; i totalChunks; i) { File partFile new File(chunkDir / i .part); if (!partFile.exists()) { return Map.of(success, false, message, 分片文件缺失请重试); } Files.copy(partFile.toPath(), out); } out.flush(); } // 3. 清理临时分片和 Redis 状态 deleteDir(new File(chunkDir)); stringRedisTemplate.delete(redisKey); return Map.of(success, true, filePath, finalFilePath); } private boolean fileExists(String fileMd5, String fileName, long fileSize) { // 实际项目中应查数据库或对象存储元数据 return false; } private void deleteDir(File dir) { File[] files dir.listFiles(); if (files ! null) { for (File f : files) { f.delete(); } } dir.delete(); } }这段代码最关键的地方有三个check接口同时承担“秒传查询”和“断点续传查询”两个职责返回已上传分片列表Redis 中直接用 set 存储分片序号天然支持去重合并前一定要做分片完整性校验否则会产生文件损坏。5.3 Spring Boot 接收 MultipartFile 的配置如果文件分片较大或并发高需要修改 Spring Boot 的文件大小限制。不同版本配置项略有差异下面给出 Spring Boot 2.x 的常见配置# 文件路径src/main/resources/application.properties spring.servlet.multipart.max-file-size20MB spring.servlet.multipart.max-request-size200MB如果你是 Spring Boot 3.x配置项前缀仍然是spring.servlet.multipart但要注意与项目实际情况核对。这里真正容易踩坑的是即使单个分片只有 2MB如果需要一次接收多个分片的请求体max-request-size必须设置得足够大。5.4 配合 MinIO 实现分片上传如果项目使用 MinIO 作为对象存储MinIO 本身支持分片上传Multipart Upload机制。使用 MinIO Java SDK 时核心步骤是调用initiateMultipartUpload获取 uploadId对每个分片调用uploadPart全部上传完成后调用completeMultipartUpload。但有一点要分清MinIO 的客户端分片上传与我们在应用层做的分片上传是两个层次。应用层的分片上传可以把分片暂存在本地或临时存储最后合并上传到 MinIO也可以直接用 MinIO SDK 的分片能力逐片上传。从工程实践看如果允许引入对象存储使用 MinIO SDK 的分片上传更省心因为合并逻辑由 SDK 处理服务端不需要手动按序号拼文件。但 MinIO SDK 的分片上传对分片顺序和 uploadId 的保存有要求uploadId 需要在下一次续传时重新获取这一点在断点续传场景中很容易忽略。6. 运行验证与效果确认跑通整个流程后你需要验证以下场景小文件正常上传成功合并后的文件可以用十六进制工具或直接打开确认无损坏上传过程中手动断开网络刷新页面再次选择同一文件程序能跳过已上传分片重新上传一个已存在的文件直接命中秒传接口返回exists: true同时并发上传多个分片观察服务端临时目录中的分片文件是否完整。如果上传成功但合并后的文件打不开优先检查合并顺序。一个常见的错误是分片文件名按chunkIndex命名后没用自然排序就合并导致顺序错乱。下面这条命令可以快速验证合并文件是否完整md5sum /tmp/upload/fileMd5_fileName对比前端计算的原文件 MD5如果一致说明合并成功。7. 常见问题与排查思路在实际项目和面试中下面这些问题出现频率最高。问题现象可能原因排查方式解决方案上传大文件时后端 OOM后端一次性接收整个文件或合并时一次性读取过大查看 JVM 堆栈、内存监控使用分片上传合并时用流式写入分片禁止一次读取整个文件到内存浏览器上传大文件崩溃前端用 FileReader 一次性读取整个文件计算 MD5打开浏览器 DevTools 观察内存占用使用抽样哈希或分块计算 MD5合并后的文件损坏分片合并顺序错误、分片缺失未被发现比对 MD5检查合并代码排序合并前校验分片完整性按分片序号自然排序断点续传失效前端没有保存已上传分片信息或服务端状态丢失检查 Redis key 是否存在用文件 md5 作为 Redis key前端上传前查询已上传分片上传界面卡住前端并发数过大浏览器连接数被占满查看 Network 面板控制并发数建议 3 到 6 个并发请求 413 错误服务器或网关限制了请求体大小查看 Nginx / Spring Boot 配置调大client_max_body_size和max-request-size在 IE 浏览器不能正常上传旧浏览器不支持 File.slice 或 FormData 相关 API查看浏览器控制台报错现代项目已不再建议兼容 IE如必须兼容需引入 polyfill 或降级处理这里特别说明一下 IE 浏览器问题。某些企业内部系统依赖 IE 环境同时使用了 ActiveX 控件这类方案与现代浏览器的 File.API 不兼容。如果面试中提到这个场景更好的回答思路是说明现代前端上传方案分片、断点续传依赖 File API、FormData、Blob 等能力对老浏览器需要降级方案或升级浏览器不要在技术选型中把 ActiveX 控件作为主路径因为它的维护成本和安全风险都很高。8. 面试回答框架如何把这个问题讲出层次回到标题的场景面试官问“大文件上传与断点续传有哪些坑”。回答时不要一上来就贴代码建议按下面的顺序组织表达更容易让面试官觉得你有工程思维。第一步先给结论大文件上传的核心矛盾是“网络不稳定 HTTP 全量传输的高失败成本”因此要分片断点续传的核心矛盾是“如何知道哪些分片传过了”因此要记录状态秒传的核心矛盾是“如何识别文件是否已存在”因此要有文件唯一标识。第二步画流程前端选文件 → 计算唯一标识 → 查秒传 → 查已上传分片 → 并发上传缺失分片 → 合并 → 校验。第三步讲关键点分片大小选择、分片完整性校验、并发控制、合并顺序、状态存储选型。第四步说坑OOM、合并文件损坏、断点续传失效、浏览器兼容、服务端超时。第五步如果能提到你实际项目中用的对象存储如 MinIO 分片上传和状态存储方案如 Redis会更有说服力。这个回答框架的好处在于即使你的项目没有完整落地过这套逻辑面试官也能看出你理解了大文件上传的底层问题而不是只会背几个名词。9. 工程化最佳实践与生产环境建议如果只是面试背题到上一节就够了。但在真实项目中落地下面几条建议值得认真对待。9.1 状态存储不要只依赖前端前端 localStorage 保存已上传分片信息在用户清缓存或换设备后会丢失。生产环境应以服务端状态为准前端状态只做加速展示。Redis 中的分片状态要设置合理过期时间。如果用户传了一半不传了这些状态不能永远占用内存。可以设置 24 小时或 7 天过期过期后由定时任务清理临时分片文件。9.2 合并操作要支持幂等如果前端重复调用合并接口服务端要有能力识别“已合并完成”的状态避免重复合并生成多个文件。常见的做法是在数据库记录文件状态合并前先查状态已经合并成功的直接返回成功。9.3 文件完整性校验不能省传输链路中的任何一个环节都可能出问题。除了前端在合并前计算 MD5生产环境建议在合并后再次对最终文件做一次校验并把校验值写入文件元数据。这样后续下载时也能做一致性检查。9.4 并发上传要控制粒度并发数不是越大越好。浏览器对同一域名的并发连接有限制后端也会面对同时写临时文件的压力。一般控制并发在 3 到 6 个比较合理。9.5 不要用一次读整个文件的方式拼接分片合并文件时要使用流式写入逐块读取分片并写入输出流。如果一把梭把整个文件读进 byte[]无论 JVM 内存多大都容易出问题。9.6 安全边界要提前想文件上传接口需要做权限控制防止未授权用户占满磁盘。文件类型校验不能只依赖前端后端要根据扩展名、Content-Type 和文件头字节等多维度校验。分片目录建议用随机目录或 hash 隔离不要允许用户自定义路径防止路径穿越。9.7 日志与监控每个分片上传、合并、校验的关键步骤都要有日志。生产环境出现问题时如果日志里没有分片序号、文件 MD5、状态码排查成本会非常高。10. 总结与后续学习方向大文件上传和断点续传不是一套死板的代码模板而是一组解决“大文件传输可靠性”问题的工程方案。它的核心价值在于用分片降低失败成本用状态管理支撑断点续传用文件唯一标识实现秒传用流式处理避免内存崩溃。如果你正在准备面试建议按“问题本质 → 方案演进 → 关键实现 → 工程坑点 → 生产实践”这条链路去理解而不是死记几个接口名。如果你正在做实际项目建议先把最小链路跑通再逐步补充并发控制、完整性校验、对象存储对接、监控告警。后续可以继续深入的方向有Web Worker 中计算文件哈希、使用 IndexedDB 持久化前端上传状态、基于 netty 或文件流的上传性能优化、分片上传的失败重试机制设计、CDN 场景下的大文件分发等。这些方向都能帮你把这一个点扩展成一整套文件处理能力。建议收藏备用。