如何在 Spring Boot 中实现大文件分片上传与断点续传?

来源:Reactjs教程作者:深圳网站建设头衔:草根站长
导读:本期聚焦于深圳网站建设创作的《如何在 Spring Boot 中实现大文件分片上传与断点续传?》,敬请观看详情。上传几个GB的视频文件时,进度条卡在99%后失败、重新上传又要从头再来,这种体验对用户几乎是不可接受的。普通表单上传不仅受请求体大小限制,还会长时间占用连接和内存,网络一断就前功尽弃。分片上传把大文件切成若干小块,前端逐片发送,后端按序号落盘,全部完成后再合并成原文件;断点续传则是在分片基础上记录已传序号,失败后只需补传缺失分片。基于Spring Boot实现时,可以设计初始化、分片上传、合并三个核心接口,用文件MD5作为唯一标识,配合Redis或数据库记录分片状态,甚至能实现秒传。本文会从前端切片策略讲到后端存储与合并,再给出断点续传和并发优化思路,帮助你把大文件上传做成一个稳定、可恢复的通用模块。

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

如何在 Spring Boot 中实现大文件分片上传与断点续传?

分片上传与断点续传的核心流程

分片上传的第一步是在前端完成文件切割。浏览器端可以通过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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0926/61968.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。