尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
大文件上传利器:Java基于MinIO实现分片上传与断点续传
简介面向Java开发者的MinIO高性能分片上传与断点续传示例资源适用于需要处理大文件传输、优化上传体验的Web系统或内部工具。资源包内含13个文件以Java后端源码为主配合前端JS含spark-md5.js、HTML、CSS及pom.xml、properties配置构成一套可直接运行的完整前后端工程。示例代码纯净仅保留必要依赖适合已具备基础Java与MinIO使用经验、想快速落地分片上传机制的开发者。压缩包整体仅19KB轻量便于下载与阅读当前已有20968人学习使用。通过该示例读者可了解分片上传、秒传与断点续传的完整实现思路掌握前端文件切片与MD5计算、后端分片合并及配置衔接的关键细节为自建文件存储上传方案提供可靠参考。1. 分片上传与断点续传为什么大文件上传不能只靠 putObject做 Java 后端这些年让我印象最深的一次翻车是现场维护某在线教育平台的老师上传 2GB 录播课进度条停在 98% 时断网整段重传用户当场打电话投诉。后来我基于 MinIO 重写了这版上传服务把分片上传和断点续传同时落地这种事故再没出现过。这份 Java 示例工程解决的就是这两件事把一个大文件拆成多个分片并行上传失败后通过 uploadId 接着传不用从零开始。适合正在做文件服务、私有对象存储或者被大文件上传稳定性折磨的 Java 后端开发者。看完你能直接把这个方案搬进自己的项目里改。2. 工程架构与目录一个上传任务在三个模块之间怎么流转2.1 技术选型为什么用 MinIO 而不是自研分片逻辑先聊选型。大文件上传常见有三代做法第一代是把整个文件直接丢给服务端小文件没问题大文件听天由命网络抖动、应用重启、超时都会让整次上传作废第二代是前端分片、后端自己拼业务层要维护分片状态、合并顺序、失败重传等于把对象存储的分片能力自己重新实现一遍血泪经验告诉我这种方案最坑的地方在于状态管理和合并时机的边界情况特别多第三代就是直接用对象存储原生的多分片上传能力。MinIO 在这条路上很合适它是开源对象存储部署轻量单机跑一个二进制就能起来SDK 对 Java 支持完整分片上传相关的接口是原生能力。协议上兼容对象存储行业通用的 S3 风格接口以后想切到其他兼容服务代码改动很小。这个示例工程的定位不是造轮子而是把 MinIO Java SDK 的分片能力封装成一套可复用的上传服务核心模块只有三个配置构建、分片编排、状态管理。工程的目录结构是这样组织的minio-upload-demo/ ├── pom.xml └── src/main/java/com/example/miniodemo ├── MinioConfig.java # MinIO 客户端构建读取 endpoint / accessKey / secretKey ├── MultipartUploadService.java # 分片上传主流程init、uploadParts、complete、resume ├── UploadState.java # 一次上传会话的状态模型 ├── UploadStateStore.java # 状态落盘与读取支撑断点续传 └── Main.java # 示例入口跑通整个流程依赖方面只需要三样MinIO Java SDK、Jackson 做状态序列化、Slf4j 打日志。版本不用纠结SDK 保持当前稳定版就行。把目录结构拿到手先别急着跑先理解 MultipartUploadService 里编排的流程后面改起来才不慌。2.2 分片参数与目录结构先搞清楚服务端模型再动手MinIO 的 multipart 上传在服务端是黑匣子协议但语义很简单三个阶段initiateMultipartUpload创建一个上传会话拿到一个全局唯一的 uploadIduploadPart按 partNumber 顺序传数据每个分片独立校验、独立存储返回对应的 etagcompleteMultipartUpload把已上传的分片按编号合并成最终对象。这个模型天然适合断点续传因为分片之间没有依赖关系某个分片失败不影响其他分片合并之前随时可以查询哪些分片已经到位。基于这个模型分片大小的选择就很有讲究了。MinIO 的分片约束是除最后一个分片外每个分片最小 5MiB最大允许 5GiB一次上传最多 10000 个分片。我一般按文件大小动态计算partSize ceil(fileSize / 10000) 向上取整到 5MB 的倍数如果文件不超过 10GB固定 8MB 分片是个不容易出错的默认值内网环境可以放大到 16MB 减少请求次数公网弱网环境用 5MB 更稳失败重传的成本更低。并发数我习惯控制在 4 到 6既要带宽跑满又不要让每个线程同时持有多个分片的数据把内存打爆。这个工程里默认是 5MB 分片、4 并发参数都做成了配置项后面二次开发章节会说怎么调。3. 核心模块拆解从 initiate 到 complete 的完整链路3.1 初始化上传uploadId 是这次会话的身份证上传流程的第一步是初始化核心代码在 MultipartUploadService 里public String initUpload(String bucket, String object) throws Exception { InitiateMultipartUploadResponse response minioClient.initiateMultipartUpload( InitiateMultipartUploadArgs.builder() .bucket(bucket) .object(object) .build() ); return response.uploadId(); }这段代码做了什么调用 SDK 的 initiateMultipartUpload 向 MinIO 申请一个上传会话返回的 uploadId 就是这次上传的身份证。后续所有分片上传、合并、中止操作都要带上它。从这一刻开始MinIO 服务端会记录这个会话的状态直到 complete 或者 abort。参数说明参数含义建议bucket对象存储桶名按业务划分比如 video-bucket、image-bucketobject对象名即最终文件的存储路径建议带目录前缀如2024/lesson01.mp4uploadId服务端返回的会话标识必须持久化断点续传靠它这里有个细节容易忽略uploadId 是字符串长度不短而且是后续所有操作的凭据。我在工程里拿到 uploadId 后会立刻交给 UploadStateStore 落盘而不是等整个上传完成再记否则进程一崩就丢。初始化这一步很快毫秒级返回真正的耗时在分片上传阶段。3.2 并发分片上传边读边传不让内存背锅分片上传是整个工程的核心也是我之前翻车最多的地方。第一版实现我用 ByteArrayOutputStream 把每个分片先读进内存再传结果 16 个线程并发传一个 4GB 文件老年代直接飙升频繁 Full GC。后来改成边读边传问题才消失。核心实现是这样的public ListPart uploadParts(String bucket, String object, String uploadId, Path filePath, long partSize) throws Exception { long fileSize Files.size(filePath); int totalParts (int) Math.ceil((double) fileSize / partSize); ListPart parts Collections.synchronizedList(new ArrayList()); AtomicInteger failed new AtomicInteger(0); CountDownLatch latch new CountDownLatch(totalParts); ExecutorService executor Executors.newFixedThreadPool(CONCURRENCY); for (int partNumber 1; partNumber totalParts; partNumber) { final int num partNumber; executor.submit(() - { try (InputStream in Files.newInputStream(filePath)) { long skip (num - 1) * partSize; long skipped in.skip(skip); if (skipped ! skip) { throw new IOException(skip 未到达目标偏移量); } long size Math.min(partSize, fileSize - skip); ObjectWriteResponse resp minioClient.uploadPart( UploadPartArgs.builder() .bucket(bucket) .object(object) .uploadId(uploadId) .partNumber(num) .data(in) .partSize(size) .build() ); parts.add(new Part(num, resp.etag())); } catch (Exception e) { failed.incrementAndGet(); log.error(分片上传失败 partNumber{}, num, e); } finally { latch.countDown(); } }); } latch.await(); executor.shutdown(); if (failed.get() 0) { throw new RuntimeException(存在失败分片请检查日志); } return parts.stream() .sorted(Comparator.comparingInt(Part::partNumber)) .collect(Collectors.toList()); }这段代码的逻辑是先按文件大小和分片大小算出总片数然后提交 totalParts 个任务到线程池。每个任务重新打开一个文件输入流skip 到自己的起始偏移量读取固定长度后调用 uploadPart 上传。上传成功的分片把 partNumber 和 etag 记入 List失败则通过 AtomicInteger 计数最后用 CountDownLatch 等所有分片结束有失败就抛异常。几个关键参数说明partSize单个分片的字节数。默认 5MB最小不能低于 5MiB最后一个分片除外太大减少请求数但单次失败的成本高太小会撞上 10000 分片上限制CONCURRENCY线程池大小工程里默认 4。这个值不是越大越好它决定同时有多少个分片在传输也决定内存中并发的输入流数量partNumber从 1 开始递增不能重复。同一个 uploadId 下不同 partNumber 可以并发上传最终合并按这个编号排序resp.etag()服务端对每个分片生成的校验标识合并时必须原样带回去。重点说一下为什么每个分片都重新打开文件流而不是共享一个流并发任务轮流读同一个流会让偏移量互相污染每个线程维护自己的输入流才能保证读到的是自己那一块数据。这种实现牺牲了一点文件句柄的开销换来了并发安全我认为是值得的。3.3 合并分片partNumber 顺序和 etag 一个都不能错所有分片传完后进入合并阶段public ObjectWriteResponse completeUpload(String bucket, String object, String uploadId, ListPart parts) throws Exception { return minioClient.completeMultipartUpload( CompleteMultipartUploadArgs.builder() .bucket(bucket) .object(object) .uploadId(uploadId) .parts(parts) .build() ); }这里传入的 parts 必须满足两个硬性条件按 partNumber 升序排列etag 必须是 uploadPart 响应里返回的原始值不能自己拼也不能从别的地方拿。SDK 的 Part 对象就是这么设计的partNumber 是 intetag 是 String。我见过最多的问题出现在断点续传场景本地状态里存的 etag 是上次进程里的值但服务端那边可能因为分片被清理或者重传后 etag 已经变了合并时就会收到 InvalidPart 之类的错误。所以正确的姿势是合并前以服务端 listParts 返回的分片列表为准而不是直接信任本地记录。还有一个容易忽略的点complete 之后整个上传会话就结束了uploadId 随之失效。如果 complete 失败不能直接重试 complete要先检查是哪些分片缺失。常见做法是列出服务端已存在的分片补传缺失的再重新 complete。这个过程正好就是下一章的断点续传逻辑。4. 断点续传实现用 uploadId 和本地账本把失败变成进度4.1 状态本地化一次上传的账本是什么样断点续传的核心在于状态持久化。进程崩溃、网络断开、服务器重启这些场景下内存里的数据说没就没所以每次分片上传成功后都要把进度写进本地文件我把这个文件叫账本。账本的数据结构设计成这样{ bucket: video-bucket, object: 2024/lesson01.mp4, uploadId: MjAyNDA0MDEtMDAwMDAwMDAw, partSize: 5242880, totalParts: 24, uploadedParts: [ { partNumber: 1, etag: abc123 }, { partNumber: 2, etag: def456 } ] }对应的 Java 模型是 UploadState字段和 JSON 一一对应。UploadStateStore 负责读写实现很简单public void save(UploadState state) throws IOException { String fileName state.getBucket() _ state.getObject().replace(/, _) .json; Path file stateDir.resolve(fileName); Files.write(file, objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(state)); } public UploadState load(String bucket, String object) throws IOException { String fileName bucket _ object.replace(/, _) .json; Path file stateDir.resolve(fileName); if (!Files.exists(file)) { return null; } return objectMapper.readValue(file.toFile(), UploadState.class); }逻辑说明save 把当前进度覆盖写入磁盘load 按 bucket 和 object 定位账本文件。文件名里把 object 的斜杠替换成下划线是为了避免在本地文件系统里创建嵌套目录。每次 uploadPart 成功后就调一次 save而不是等全部完成再写这样最坏情况只丢一个分片的进度。这里有一个设计取舍为什么不用数据库单机场景下本地文件够用而且这个账本的生命周期和上传任务绑定任务完成就删除没有长期存储的需求。如果未来要做多实例共享把 UploadStateStore 换成 Redis 实现即可接口不变。4.2 恢复流程怎么知道哪些分片传过了断点续传的恢复流程分四步核心代码如下public void uploadWithResume(String bucket, String object, Path filePath) throws Exception { UploadState state stateStore.load(bucket, object); String uploadId; ListPart serverParts; if (state ! null) { try { ListPartsResponse response minioClient.listParts( ListPartsArgs.builder() .bucket(bucket) .object(object) .uploadId(state.getUploadId()) .build() ); serverParts response.parts(); uploadId state.getUploadId(); } catch (ErrorResponseException e) { log.warn(uploadId 已失效重新初始化原因: {}, e.getMessage()); stateStore.delete(bucket, object); uploadId initUpload(bucket, object); serverParts Collections.emptyList(); } } else { uploadId initUpload(bucket, object); serverParts Collections.emptyList(); } SetInteger uploadedNumbers serverParts.stream() .map(Part::partNumber) .collect(Collectors.toSet()); ListPart allParts uploadMissingParts(bucket, object, uploadId, filePath, state.getPartSize(), uploadedNumbers); completeUpload(bucket, object, uploadId, allParts); stateStore.delete(bucket, object); }恢复逻辑的关键在于先读本地账本拿到 uploadId再调 listParts 问 MinIO 服务端这个 uploadId 下实际存了哪些分片。这里不直接信任本地记录的 uploadedParts而是以服务端为准因为可能存在本地记录了但服务端没收到的分片也可能存在服务端有但本地没记录的。两者对齐后uploadedNumbers 就是真正不需要重传的 partNumber 集合。uploadMissingParts 在遍历分片时判断如果 partNumber 已经在 uploadedNumbers 里就跳过否则重新打开文件流读取对应区域上传。所有分片到位后再走 completeUpload。整个流程下来断点续传的本质就是查缺补漏而不是从头再来。有一个边界情况要特别注意本地账本存在但 uploadId 已在服务端失效比如上传中断超过 MinIO 的清理周期默认 7 天未 complete 的会话会被服务端回收。此时 listParts 会抛异常代码里做了兜底删除本地账本重新 initUpload全量重传。这个兜底忘记写的话恢复时永远卡在同一个 uploadId 上属于那种排查半天才能定位的隐性 bug。5. 避坑指南分片上传最容易翻车的六个真实场景5.1 分片与并发相关的三个坑先说第一个也是最常见的坑分片数量超过协议上限。现象是上传大文件时初始化没问题传着传着某个分片报错服务端返回的错误提示里提到分片数量超限。原因是 10000 个分片是协议上限如果 partSize 设成 5MiB文件超过 48.8GB 就会超更隐蔽的情况是 partSize 算错比如把 5MB 写成了 5KB一个 1GB 文件就拆出 20 万片。解决方式是按文件大小动态计算 partSize核心公式我在第 2 章给了先算文件大小除以 10000向上取整到 5MiB 的倍数。从那以后我每次都在代码里加一道断言上传前校验 totalParts 是否超过 10000超了就自动调大 partSize。第二个坑是并发数开太大导致内存先垮。现象是老年代占用持续上涨GC 频繁甚至直接 OOM而且越是大文件越明显。原因是每个分片上传时都有一份数据在内存里流转线程池开 16 个并发就意味着同时有 16 个分片的数据驻留按 8MB 分片算就是 128MB再加上 SDK 内部缓冲内存压力很大。解决方式是并发数控制在 4 到 6分片大小和并发数不是越大越好它们和带宽、内存是三角关系。我的一般做法是先定并发数再按单分片大小乘并发数估算内存占用让这个值不超过堆内存的十分之一。第三个坑是合并时 etag 对不上。现象是 complete 的时候报 InvalidPart 或类似的合并错误但每个分片明明都上传成功了。原因是传入 complete 的 Part 列表里 etag 不是来自 uploadPart 的响应原值常见于断点续传时用了本地账本里的旧 etag而服务端那边分片已经重传过etag 变了。解决方式是以服务端 listParts 返回的分片信息为准把本地记录的 etag 和 partNumber 序列重新对齐再组装 Part 列表。这个坑我踩过一次之后就再也没犯过凡是断点续传合并前强制刷新一次服务端状态。5.2 断点恢复相关的三个坑第四个坑是断点续传压根没生效每次失败都是从第一片开始。现象是进程重启后重新上传日志里 partNumber 又从 1 开始打。原因是账本只存在内存里进程一崩就没了压根没落盘。解决方式很朴素每次 uploadPart 成功后立即调 stateStore.save把进度写进磁盘。注意不是全部完成再写那样等于没写。我一般会在 save 里加一个同步写配置宁可慢一点也保证账本不丢。第五个坑是 uploadId 过期导致恢复失败。现象是上传中断后隔了一天回来点继续listParts 直接抛异常提示这个上传会话不存在。原因是 MinIO 服务端有清理机制默认 7 天没有 complete 的 multipart 会话会被回收。解决方式是在恢复流程里捕获这个异常删掉本地账本、重新 initUpload、全量重传。如果业务上确实可能中断很久可以把分片大小调大减少会话数量或者在业务层加一个定时任务超过 24 小时未完成的会话主动 abort 清理掉避免堆一堆过期账本。第六个坑比较隐蔽listParts 分页不全导致恢复时漏传分片。现象是恢复后 complete 失败提示缺少某些 partNumber但本地账本里明明记录了这些分片已上传。原因是 listParts 默认每页最多返回 1000 条如果分片总数超过 1000直接取 parts() 只拿到了第一页。解决方式是分页拉取循环调用 listParts 并携带 keyMarker 和 uploadIdMarker 参数直到拿满全部分片。这个坑在分片数超过 1000 的大文件场景才会触发规模小的项目很难遇到但一旦遇到就是疑难杂症我把它写出来是希望后面的人少走这个弯路。6. 二次开发改造把示例工程接进 Spring Boot 服务6.1 从 demo 到 Spring Boot接口怎么拆这份工程是纯 main 方法入口接进生产项目时我一般会拆成四个接口创建上传会话、上传分片、查询已上传分片、合并分片。前端先请求创建会话拿到 uploadId 和分片信息再并发上传分片最后通知后端合并。上传分片接口可以走应用服务器中转也可以直接用 MinIO 生成的预签名 URL 让前端直传后者能省掉一层文件流但要在创建会话时把每个分片的 URL 都生成好复杂度会上去。我建议第一步先做中转模式跑通了再优化带宽。工程的 MultipartUploadService 可以直接复用UploadStateStore 换成接口后续可以接 Redis。参数方面partSize 和并发数建议做成配置中心的下发项不同网络环境给不同配置。6.2 参数调优一个可以参考的配置表参数内网建议值公网建议值说明partSize16MB5MB内网带宽充足大分片减少请求量并发数84内网可以堆并发公网受带宽限制线程池队列有界队列 200有界队列 100防止无限排队打爆内存账本存储路径本地磁盘 tmp本地磁盘 tmp多实例时改 Redis验证断点续传的方法很简单上传中直接杀掉进程重启后看日志是否只传剩余分片。我会故意在 MinIO 容器上做网络隔离测试模拟极端情况确保合并前服务端状态刷新逻辑是对的。6.3 验证方法和一个习惯从那以后我每次写分片上传都会先画一张 uploadId 生命周期图从 create 到 complete/abort 的每一条路径都标出来再动手写代码。这份工程的源码和完整配置都在资源包里拿到后改一下 MinioConfig 的 endpoint 和密钥就能跑通。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

《给 Vibe Coding 小白的前端效果词典:80 个常见术语速查手册》

《给 Vibe Coding 小白的前端效果词典:80 个常见术语速查手册》

📚 给 Vibe Coding 小白的前端效果词典 80 个常见术语速查手册(附 Prompt 示例 核心实现) ✅ 为什么需要这本词典? 用 Vibe Coding 做前端时,模糊描述(如“鼠标放上去有点反应”)会让 AI 输出…

📅 2026/10/16 6:37:07
HCIE-Datacom通关经验分享 - 毕业一年网工备考心得

HCIE-Datacom通关经验分享 - 毕业一年网工备考心得

​大家好,我是腾科HCIE学员陈同学,一名毕业工作一年左右的网络工程师。回顾备考 HCIE-Datacom 的这段时间,心里有很多感慨。毕业参加工作一年左右,我越来越意识到网络技术学习的重要性。虽然平时工作中接触过一些网络设备和配置&a…

📅 2026/10/16 6:37:07
2S电池组均衡方案:MP2642GR-Z与PIC18F87J50实现有源均衡

2S电池组均衡方案:MP2642GR-Z与PIC18F87J50实现有源均衡

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/16 6:37:06
MORE NEWS

更多资讯

📰

程序员转型AI大模型:机遇、陷阱与实战路线全解析

我自己在工程师圈子里观察到一个现象:最近半年,讨论“程序员转型AI大模型”的人,比讨论框架版本更新的人还多。打开技术社区,永远有人在问“现在转大模型还来得及吗”;朋友圈里,不是晒大模型课程打卡&#…

📰

Git新手5步实操地图:从clone到push的最小闭环

1. 这不是命令行说明书,而是一张你随时能抄作业的Git操作地图“10分钟图文并茂手把手教会你Git常用指令”——看到这个标题,我第一反应不是点开,而是把手机倒扣在桌面上,泡了杯浓茶。为什么?因为过去三年里&#xff0c…

📰

面向过程与面向对象:本质是建模思维的分岔路口

1. 为什么“面向过程”和“面向对象”不是两种写法,而是两种思考方式?刚入行那会儿,我带过几个零基础转行的学员。有位A同学学完C语言函数调用后信心满满,说:“面向过程不就是把任务拆成一个个函数嘛?”接着…

📰

Git高频指令速查手册:10个救命命令手把手教学

1. 这不是Git教程,是“能立刻用起来”的指令速查手册你有没有过这种时刻:刚在团队群里被,说“主干分支被误提交了,快回滚”,你手指悬在键盘上,脑子里却只浮现出git push和git pull两个词?或者写…

📰

给AI装上“心智”:陪伴场景下的五模块状态框架

我一直觉得,聊天陪伴型AI最难的地方不在“会说话”,而在“有状态”。你把市面上一款主流对话模型接上记忆库、加好人设提示词,刚开始聊体验确实不错;可聊上一周,那种“空洞感”很快就浮出来了——同一个问题&#xff0…

📰

OpenCV Python人脸识别实战:从环境搭建到实时检测与识别

1. 从零搭建人脸识别环境:版本选择和安装陷阱先说明一下,这篇文章是一篇我自己从头做过很多遍的实战记录,不是什么理论科普。标题里提到的OpenCV和Python人脸识别,是我在实际项目里用了很久的一条技术路线。它最大的优势就是轻量、…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬