大文件上传在业务系统中很常见,例如网盘、视频平台、企业资料库等。如果直接用Spring Boot的MultipartFile接收整个文件,服务端通常会因为请求体过大、网关超时或内存溢出而失败。即便调大上传限制,弱网环境下一次中断就意味着整个文件需要重传。分片上传将文件按固定大小切分成多个小块,每块独立上传,服务端先保存分片,最后按序号合并。断点续传则是在这个基础上记录上传进度,让客户端跳过已存在的分片,只补传缺失部分。这个方案能明显提升大文件上传的稳定性和用户体验。

分片上传与断点续传的核心流程
分片上传的第一步是在前端完成文件切割。浏览器端可以通过File.slice(start, end)方法把用户选择的文件按照固定大小切成若干块,通常会同时计算整个文件的MD5值。这个MD5值在整个流程中扮演文件唯一标识的角色,后端根据它可以区分不同文件,也能为后续秒传提供依据。分片大小一般取1MB到5MB之间,太小会导致请求数量过多,太大则弱化了分片上传的意义,需要根据实际网络环境和服务器IO能力调整。
前端完成切割后,会先调用初始化接口,向服务端传递文件名、文件大小、总分片数和文件MD5等信息。服务端收到后生成一个上传任务ID,用来关联后续所有分片。如果该文件之前已经上传过一部分,初始化接口还会返回已经存在的分片序号列表,前端拿到这个列表后就可以跳过这些分片,只上传缺失的部分。这个机制就是断点续传的核心:它不依赖浏览器的断点恢复能力,而是把上传状态持久化在服务端,客户端每次重新打开页面都能继续上次的进度。
所有分片上传完成后,前端再调用合并接口。服务端按照分片序号从小到大依次读取每个分片文件,写入到最终的目标文件。合并完成后,删除临时分片目录,对外返回正式文件的访问地址。整个流程看起来简单,但实际落地时需要重点处理并发上传、分片顺序、临时文件清理和异常恢复等问题,下面结合Spring Boot代码逐一说明。
Spring Boot 接口与存储设计
后端需要提供三个核心接口:初始化、分片上传、合并。初始化接口接收文件元信息,生成一个唯一的uploadId,通常可以用UUID,并在服务器上创建对应的临时目录。目录结构建议按uploadId隔离,每个分片文件用分片序号命名,例如0、1、2这样的纯数字文件名。分片上传接口接收uploadId、分片序号以及分片文件本身,直接将文件写入临时目录即可。合并接口则根据uploadId找到临时目录,按序号顺序合并。
下面是一个简化后的Controller代码示例,重点展示接口定义和参数接收方式。为了减少内存占用,分片保存时直接使用transferTo方法将上传的临时文件移动到目标位置,而不是将整个分片读入内存。
@RestController
@RequestMapping("/upload")
public class BigFileUploadController {
@Autowired
private BigFileUploadService uploadService;
@PostMapping("/init")
public Result init(@RequestBody InitUploadDTO dto) {
String uploadId = uploadService.initUpload(dto);
return Result.success(uploadId);
}
@PostMapping("/chunk")
public Result uploadChunk(@RequestParam("uploadId") String uploadId,
@RequestParam("chunkIndex") int chunkIndex,
@RequestParam("file") MultipartFile file) throws IOException {
uploadService.saveChunk(uploadId, chunkIndex, file);
return Result.success();
}
@PostMapping("/merge")
public Result merge(@RequestBody MergeUploadDTO dto) {
String fileUrl = uploadService.mergeChunks(dto.getUploadId(), dto.getFileName());
return Result.success(fileUrl);
}
}
服务层的分片保存逻辑也比较直接,核心是确保分片目录存在,然后将上传的文件保存为以分片序号命名的文件。这里使用Path.resolve拼接路径,注意Windows和Linux路径分隔符的差异,Spring Boot底层会处理,业务代码中用相对路径即可。合并时使用RandomAccessFile或FileChannel按序读取分片,避免一次性将大文件加载到内存。以下代码展示了保存分片和合并文件的核心实现。
@Service
public class BigFileUploadService {
private static final String TEMP_DIR = "upload/temp/";
public String initUpload(InitUploadDTO dto) {
String uploadId = UUID.randomUUID().toString().replace("-", "");
File dir = new File(TEMP_DIR + uploadId);
if (!dir.exists()) {
dir.mkdirs();
}
return uploadId;
}
public void saveChunk(String uploadId, int chunkIndex, MultipartFile file) throws IOException {
File dir = new File(TEMP_DIR + uploadId);
if (!dir.exists()) {
dir.mkdirs();
}
File chunkFile = new File(dir, String.valueOf(chunkIndex));
file.transferTo(chunkFile);
}
public String mergeChunks(String uploadId, String fileName) throws IOException {
File dir = new File(TEMP_DIR + uploadId);
File[] chunks = dir.listFiles();
if (chunks == null || chunks.length == 0) {
throw new RuntimeException("没有可合并的分片");
}
Arrays.sort(chunks, Comparator.comparingInt(f -> Integer.parseInt(f.getName())));
File targetDir = new File("upload/final/");
if (!targetDir.exists()) {
targetDir.mkdirs();
}
File targetFile = new File(targetDir, fileName);
try (FileChannel targetChannel = new FileOutputStream(targetFile).getChannel()) {
for (File chunk : chunks) {
try (FileChannel chunkChannel = new FileInputStream(chunk).getChannel()) {
chunkChannel.transferTo(0, chunkChannel.size(), targetChannel);
}
}
} finally {
// 合并完成后删除临时目录
for (File chunk : chunks) {
chunk.delete();
}
dir.delete();
}
return "/files/" + fileName;
}
}
上面的代码中,file.transferTo(chunkFile)在Spring Boot中底层会调用Part的写入方法,对于大分片来说性能较好。合并时按文件名转换为整数排序,保证顺序正确。需要注意的是,如果分片数量很多,逐个打开文件流会消耗文件句柄,这里使用try-with-resources确保每个分片读取后及时关闭。生产环境还应该校验分片大小是否完整,防止上传中断产生损坏分片。
断点续传状态记录与秒传实现
断点续传需要服务端能够查询某个上传任务已经存在哪些分片。最简单的做法是在初始化接口里扫描临时目录中的文件名,将已存在的分片序号返回给前端。这种方式无需额外存储,适合单机部署。如果系统是分布式部署,或者临时目录可能被清理,就需要把分片状态记录到Redis或数据库中。以Redis为例,可以用Set结构保存每个uploadId对应的已传分片序号,上传一个分片就执行SADD,查询时使用SMEMBERS获取全部序号。
除了断点续传,文件MD5还能用来实现秒传。初始化时如果后端发现该MD5已经对应一个完整文件,可以直接返回该文件的访问地址,前端无需上传任何分片。判断逻辑通常放在初始化接口中,先根据MD5查询文件表,如果存在且文件大小一致,则直接返回秒传结果。如果不存在,则创建上传任务并返回uploadId。以下代码展示了在初始化阶段同时支持断点续传和秒传的思路。
public InitResult initUpload(InitUploadDTO dto) {
// 1. 秒传判断
UploadedFile existing = fileRepository.findByMd5(dto.getFileMd5());
if (existing != null && existing.getSize() == dto.getFileSize()) {
return InitResult.secUpload(existing.getUrl());
}
// 2. 生成uploadId并创建临时目录
String uploadId = UUID.randomUUID().toString().replace("-", "");
File dir = new File(TEMP_DIR + uploadId);
dir.mkdirs();
// 3. 查询已上传分片序号,供前端跳过
File[] chunks = dir.listFiles();
List<Integer> uploadedIndexes = new ArrayList<>();
if (chunks != null) {
for (File chunk : chunks) {
uploadedIndexes.add(Integer.parseInt(chunk.getName()));
}
}
return InitResult.normal(uploadId, uploadedIndexes);
}
使用Redis记录分片状态时,需要考虑过期时间。如果上传任务长时间不活跃,应该允许Redis中的状态过期,同时清理服务器上的临时分片目录。过期时间可以设置为24小时或48小时,具体根据业务场景调整。清理任务可以通过定时任务扫描临时目录的创建时间,超过阈值的目录直接删除,避免磁盘被废弃分片占满。断点续传状态记录的本质是把上传进度从客户端内存转移到服务端,这样用户更换浏览器、刷新页面甚至重启设备后,依然能接着上次的进度继续上传。
并发合并与常见问题优化
分片上传天然适合并发,前端可以同时发送多个分片请求,充分利用带宽。但服务端合并时必须保证所有分片都已上传完成,并且要按照序号顺序写入。如果合并请求到达时仍有分片未上传,需要返回错误提示前端等待,或者由前端在合并前先调用一个校验接口确认分片齐全。实际开发中,前端可以在最后一个分片上传完成后再发起合并请求,这样基本可以避免合并时序问题。
另一个常见问题是大文件合并时的内存占用。有些开发者习惯将每个分片读成字节数组再写入目标文件,如果分片大小是5MB,1000个分片就是5GB,这样做无论是对内存还是GC都是灾难。正确做法是使用FileChannel.transferTo或Files.copy配合InputStream进行流式复制,让操作系统直接在内核态完成数据传输,应用层不持有大块内存。上面的合并示例已经展示了这种写法,这也是大文件处理的基本功。
还有一些配置层面的限制需要留意。Spring Boot默认的单文件上传大小和请求体大小都可能成为瓶颈,可以在配置文件中调整,例如设置spring.servlet.multipart.max-file-size=100MB和spring.servlet.multipart.max-request-size=100MB。如果前面有Nginx反向代理,还需要同步调整Nginx的client_max_body_size参数,否则请求根本到不了应用层。分片大小如果设为5MB,通常不会触发这些限制,但初始化参数校验仍然要有。
最后是分片文件的安全性问题。临时目录不应该直接暴露给外部访问,合并后的正式文件若有权限要求,需要走统一的文件下载接口做权限校验。上传过程中还可以增加分片MD5校验,前端上传每个分片时携带该分片的MD5,服务端保存后校验是否一致,不一致则返回错误让前端重传该分片。这样做虽然增加了一点计算开销,但能有效防止网络传输中的数据损坏,尤其适合对文件完整性要求高的业务。
整体来看,Spring Boot实现大文件分片上传与断点续传并不复杂,关键在于把状态管理、文件存储和合并逻辑拆解清楚。先把流程跑通,再根据实际场景补充Redis状态记录、秒传判断和定时清理,就能形成一个稳定可复用的上传模块。这个方案不依赖前端特定框架,任何支持文件切片和并发请求的客户端都能接入,后端也可以用类似思路扩展到其他语言或框架。
分片上传断点续传Spring Boot修改时间:2026-09-26 03:23:39