新闻详情

新闻详情

首页 / 资讯中心 / 详情

JavaWeb大文件分片秒传与进度监控方案详解

发布时间:2026/10/2 20:57:11来源:尧图网络
JavaWeb大文件分片秒传与进度监控方案详解
在国企OA系统里视频文件上传一直是个绕不开的烦心事。审批流程要挂视频附件、培训资料要上传录像、项目例会要留痕存档动辄几百兆的视频文件扔给普通上传接口传一半断了、超时了、服务器崩了都是家常便饭。去年我接手一个JavaWeb项目需求很明确让用户能以接近“秒传”的速度把大视频提交到OA系统同时全程能看到上传进度断网了还能续传。这篇文章就把那套基于JavaWeb的分片秒传与进度监控方案完整拆给你看从设计思路、核心代码、前后端联调到排坑记录一次讲透。1. 方案选型与整体设计思路1.1 为什么不能用普通上传直接搞定视频文件很多人第一反应是“直接甩个multipart请求不就完了”在OA系统里这么干基本是给自己埋雷。国企OA通常部署在内网服务器配置不算豪华带宽峰值看着有百兆实际多用户同时访问时单文件上传速度能跌到1~2MB/s。一个500MB的视频普通上传要传七八分钟期间只要有一个请求超时、浏览器标签被误关、网络出现一次抖动整个文件就报废用户就得从头再来。还有一个隐藏问题Tomcat默认对multipart请求体大小有限制直接传大文件很容易触发MaxUploadSizeExceededException。就算你调大了max-file-size也会让服务器的IO线程长时间被一个请求占住其他用户连普通的OA表单提交都会跟着变卡。所以我当时给业务方的第一句话就是大文件上传必须分片没有第二条路。分片上传的核心逻辑是把一个大文件切成若干个独立小块逐块上传。每块都是独立的HTTP请求失败只重试这一块服务器再按顺序把所有分片合并成完整文件。再加上秒传机制——先用文件内容算出唯一指纹如果服务器已经有这个文件直接告诉前端“不用传了我已经有了”这才是真正意义上的“秒传”。1.2 技术栈与总体架构我们这套方案整体还是走JavaWeb传统路线没有引入太激进的东西。具体选型是这样组件选型说明后端框架SpringBoot 2.x MyBatis-Plus开发接口快维护性好数据库MySQL 8.0存文件元数据和分片记录缓存Redis 5.x存实时进度、临时状态前端原生JS axios SparkMD5兼容性优先OA系统很多用户还在用老内核浏览器文件存储本地磁盘 Nginx静态映射内网环境没必要上OSS省成本整体流程是这样的前端拿到视频文件后先计算整个文件的MD5值带着这个MD5去后端查一下“有没有入库”有就直接秒传成功没有则按固定大小切片分组并发上传后端每收到一片就记录分片信息全部收齐后触发合并合并过程中前端轮询进度接口把进度条从0走到100%。为什么进度用Redis而不是纯前端统计因为要考虑断点续传。用户传了一半断网重新打开页面后前端需要知道哪些片传过了、哪些没传过这些信息必须存在后端。Redis的SET、INCR都能原子操作天然适合记录分片状态。文件元数据和分片映射关系还是落MySQL保证持久化Redis挂了可以从数据库恢复。2. 核心细节解析与实操要点2.1 切片大小与并发数的确定切片大小不是拍脑袋定的我总结出一个经验区间2MB到8MB之间最稳。切片太小比如1MB一片HTTP请求的往返开销和数据库写入次数会翻好几倍上传500MB文件要发500次请求光握手和响应就耗费大量时间切片太大比如50MB一片就失去了分片的意义一片失败重传的成本太高。切片数的计算公式很简单总片数 Math.ceil(fileSize / chunkSize)。如果视频是1GB切片定8MB就是128片如果网络差、用户多我建议切成4MB256片对后端来说也能扛住。我们实际生产环境用的就是8MB内网环境下单片上传耗时基本在1秒以内整体体验很好。并发数同样需要控制。浏览器对同一域名的并发连接数有限制HTTP/1.1下通常6个但你不能真的把6个并发全打满因为后端Tomcat的线程池要伺候整个OA系统。我一般把前端并发数压到3后端接口超时时间放宽到120秒。算一笔账如果同时有10个人在传视频每人3个并发共30个请求在跑Tomcat默认200线程池剩余线程足够支撑OA其他接口不会把系统拖死。2.2 文件唯一标识与秒传逻辑秒传的关键是文件唯一标识我直接用的全文件MD5。这里必须强调不能只比对文件名同一部培训视频可能在十个科室里叫十个名字内容完全一样也不能只看文件大小大小一样内容可能完全不同。MD5碰撞概率在业务场景里基本可以忽略用来做秒传的判断指纹完全够用。计算整个文件MD5的耗时和文件大小有关1GB视频在普通办公电脑上可能要算10到20秒。这个时间不能浪费前端需要一边算MD5一边给用户展示“正在识别文件指纹”让用户觉得系统在做正事而不是卡死了。计算完成后调用秒传查询接口GET /file/exists?md5{md5}后端返回三种状态fileExiststrue表示服务器已有相同文件前端直接结束chunkExiststrue表示该文件的历史分片存在但未合并需要从缺失的分片开始续传都为空则从第一片开始传。这个接口相当重要我把判断逻辑放在最前面如果每次都傻乎乎地传完整分片就谈不上秒传了。2.3 进度监控的前后端交互方案进度监控最容易踩的坑是“前端自己数后端不关心”。用户刷新页面后前端的内存计数归零进度条回退体验很差。我采用的方案是后端记录 前端轮询两端数据对齐。后端记录分片数用两个维度一个是MySQL里的分片表记录每片的上传状态另一个是Redis里的计数器专门给进度接口调用的。每次分片上传成功先写数据库然后对Redis的key做INCR。进度接口返回当前已上传片数和总片数前端计算出百分比。如果不想每上传一片就轮询一次可以等本片上传完成后立即更新本地进度同时开一个每秒一次的定时器去后端同步“真实进度”两个进度之间做校准。这种方式能保证大文件长时间上传时就算前端事件丢失也能从后端捞回来。至于WebSocket推送在内网OA环境里不强求轮询已经足够轻量。3. 实操过程与核心环节实现3.1 前端实现文件切片与秒传查询前端这块我用的是原生JS手写切片逻辑没有用第三方大文件上传组件。不是否定组件而是OA系统环境复杂老浏览器兼容性问题很多自研代码反而更容易控制边界。核心代码框架如下// 文件选择后 const file document.getElementById(videoFile).files[0]; const chunkSize 8 * 1024 * 1024; // 8MB一片 const chunkCount Math.ceil(file.size / chunkSize); // 1. 计算整个文件MD5 const fileMd5 await computeFileMd5(file); // 基于spark-md5封装建议放到Web Worker里 // 2. 秒传查询 const { data } await axios.get(/file/exists, { params: { md5: fileMd5 } }); if (data.fileExists) { showProgress(100); return; } // 3. 从缺失分片位置开始上传 let uploadedChunks data.uploadedChunks || []; let current 0; // 4. 并发上传分片 const concurrency 3; async function uploadWorker() { while (current chunkCount) { const index current; if (uploadedChunks.includes(index)) continue; const start index * chunkSize; const end Math.min(start chunkSize, file.size); const chunk file.slice(start, end); const formData new FormData(); formData.append(file, chunk); formData.append(md5, fileMd5); formData.append(chunkIndex, index); formData.append(chunks, chunkCount); formData.append(fileName, file.name); try { await axios.post(/file/uploadChunk, formData); updateProgress(); } catch (e) { retryPush(index); // 进入重试队列 } } }注意file.slice是Blob对象不需要加载整个文件到内存所以不用担心用户打开1GB视频占用掉2GB内存。计算MD5时我用FileReader分块读取文件每块大小1MB读取完一块继续下一块SParkMD5支持增量追加这个方法在IE上也能跑通。3.2 后端实现分片接收与合并后端的核心接口就这么几个秒传查询、分片上传、进度查询、合并。分片上传的Controller方法PostMapping(/uploadChunk) public Result uploadChunk(RequestParam(file) MultipartFile file, RequestParam(md5) String md5, RequestParam(chunkIndex) Integer chunkIndex, RequestParam(chunks) Integer chunks) throws IOException { // 1. 防止重复提交先判断分片是否已存在 FileChunk entity fileChunkMapper.selectByMd5AndIndex(md5, chunkIndex); if (entity ! null entity.getStatus() 1) { return Result.ok(分片已存在跳过); } // 2. 保存分片到临时目录 File dir new File(fileStoragePath File.separator md5); if (!dir.exists()) dir.mkdirs(); File chunkFile new File(dir, chunkIndex .part); file.transferTo(chunkFile); // 3. 记录分片信息 fileChunkMapper.insert(md5, chunkIndex, file.getSize(), 1); return Result.ok(); }这里有个细节有些前端框架会把分片序号从0开始或从1开始后端必须和前端严格对齐否则合并出来的视频会缺块或者顺序错乱。我统一约定从0开始数据库里chunk_index字段存的就是0、1、2...合并时按这个字段升序排列。合并接口的写法PostMapping(/merge) public Result merge(RequestParam(md5) String md5, RequestParam(chunks) Integer chunks, RequestParam(fileName) String fileName) { // 校验分片完整性 Integer count fileChunkMapper.countFinishedByMd5(md5); if (count null || count ! chunks) { return Result.error(分片未全部上传); } File dir new File(fileStoragePath File.separator md5); File finalFile new File(fileStoragePath File.separator md5 getExt(fileName)); try (FileOutputStream fos new FileOutputStream(finalFile)) { for (int i 0; i chunks; i) { File part new File(dir, i .part); Files.copy(part.toPath(), fos); } } // 校验合并文件大小与原始文件一致 if (finalFile.length() ! expectedSize) { return Result.error(合并失败文件大小不匹配); } // 写入文件表并清理临时分片 fileInfoMapper.insert(new FileInfo(md5, fileName, finalFile.getPath(), finalFile.length(), 1)); deleteQuietly(dir); return Result.ok(); }合并时使用Files.copy(part.toPath(), fos)时流不会自动关闭分片文件但好在每个分片都不大。正确姿势是用try-with-resources包裹每个分片的输入流或者干脆用RandomAccessFile按偏移量写入我这里为了代码直白起见就用顺序写。对于100片以内的视频顺序写不会有性能问题。3.3 进度监控接口与前端进度条进度接口我直接复用分片表的数据不额外存“进度百分比”这种容易失效的字段GET /file/progress?md5{md5}返回JSON结构{ totalChunks: 128, uploadedChunks: 67, uploadedSize: 603979776, totalSize: 1073741824, percent: 56.2 }后端实现上先查Redis的布隆过滤器或者存储的uploadedCount如果Redis里没有就查MySQL的分片表count。为什么有Redis还要查MySQL因为Redis可能在服务器重启后丢数据必须有一个持久化兜底。Redis的key设置成upload:progress:{md5}每次分片上传成功后在Service层执行redisTemplate.opsForValue().increment(key)并设置24小时过期。前端进度条在每片上传成功后调用一次这个接口而不是单纯本地累计。为了避免请求太频繁我加了一个节流每200毫秒最多刷新一次进度条。这样即使用户切到后台等切回来时进度条也能通过轮询追上真实状态。3.4 断点续传与失败重试断点续传本质上不是花哨功能而是把“已上传分片”作为基础数据服务。用户刷新页面后前端重新计算MD5不放心就调秒传查询接口后端返回uploadedChunks列表。然后前端把所有已上传的索引存进Set里上传前先判断这个索引如果已经被Set记录了直接跳过。失败重试单独说一下。网络不稳定时可能某一片传了99%断掉axios会抛异常。我的策略是单片的失败次数不超过3次超过3次就暂停整个任务提示用户“网络异常请点击重试”。重试时不需要重新计算MD5直接复用之前的值把未上传的分片继续传完就行。这个设计避免了一直在网络边缘疯狂试探。4. 常见问题与排查技巧实录4.1 大文件MD5计算时间过长界面卡死这个问题在我们上线第一周就遇到了。有用户传一个2.4GB的执法记录仪视频前端用的是普通FileReader同步计算MD5结果浏览器标签页直接无响应用户骂娘我也很无奈。解决办法有两个一是把MD5计算塞进Web Worker线程计算过程完全不占用UI线程用户可以看到进度条还在动二是做一个降级策略——如果文件超过2GB就只计算前128MB的MD5作为秒传标识命中率稍微低一点但至少不会让浏览器崩掉。我最后两个都做了效果好得多。4.2 分片合并后视频文件无法播放或损坏这个坑非常隐蔽。当时我发现部分视频合并后打开提示“文件已损坏”排查一圈终于找到原因前端切片时的end计算用了Math.min(start chunkSize, file.size)代码看着没毛病但在一个边界值上最后一篇切片如果大小为0也会被上传。后端合并时多合并了一个0字节文件导致视频数据错位。修复方法是前端保证end - start 0才上传后端合并前也校验一下每个分片文件的大小0。另外还要注意分片文件的磁盘写入是否完整MultipartFile.transferTo()结束后不要立刻就对分片做校验最好重新打开文件检查长度是否和请求中的file.getSize()一致确保没有写一半的情况。4.3 多人同时上传同一个文件秒传与合并冲突场景是这样的某天下午信息科三个人同时上传同一份培训视频这个视频之前没入库三个人都算出了相同的MD5都没秒传成功于是三批分片同时写进了同一个临时目录。合并时每个人都在做“把分片合成文件”结果A合并成功后又删了临时目录B的合并请求进来发现分片不够直接报错。解决方案是给file_info表的MD5字段加唯一索引合并时使用“先查后插”的原子操作。具体来说合并接口里先执行一次SELECT ... WHERE md5... AND status1如果有记录直接删除当前临时分片并返回秒传成功如果没有再执行INSERT但这里要捕获唯一键冲突的异常捕获后同样删除临时分片返回秒传成功。这样多个并发请求只有一个能真正合并其余都自动“撞车”成秒传。4.4 上传过程中服务器临时目录被清空或磁盘不足我在一台测试服务器上遇到过临时目录所在的磁盘只有20GB一个视频就占了8GB分片用户还没合并另一个任务又占8GB磁盘直接满了整个OA系统其他功能都开始报错。后来我做了两件事第一前端在开始上传前调用后端接口查询磁盘剩余空间若剩余空间不足当前文件大小的1.5倍直接拒绝上传并提示用户联系管理员清理。第二加了一个定时任务每六小时扫描一次临时目录删除创建时间超过24小时且没有合并记录的分片文件。这个时间窗口要大于最慢用户的上传时间避免误删正在上传的文件。4.5 浏览器兼容性老版IE内核无法用FormData或fetch国企OA环境最头疼的是浏览器。有的科室电脑还停留在Windows 7 IE11旧版内核不支持fetch和Promise甚至连Blob.slice的写法都有差异。我在代码里做了兼容判断检测到浏览器不支持File.prototype.slice时自动降级为普通上传同时提示用户安装新版Chrome内核浏览器。普通上传接口依然保留虽然体验差点但功能能用不至于让用户完全没法提交视频。5. 一些亲测有效的优化与建议5.1 服务端限流与线程池隔离分片上传接口如果完全放开并发Tomcat线程池很容易被打满。我在接口入口加了基于Semaphore的限流信号量设成50同时只允许50个分片上传请求进入处理逻辑其余请求排队等待。这样即使全公司都在传视频最多也就占用50个线程OA系统其他接口依然响应正常。实际调参建议如果你们OA并发用户数在200以内信号量建议设30~50超过500人的国企集团建议设80同时配合Nginx的limit_req模块做IO层限流。这个值不是越大越好因为分片上传消耗的主要是磁盘IO线程太多反而导致分片写入时互相抢锁。5.2 与OA流程表单的业务集成细节视频传完不等于流程走完还得和OA的业务表单关联。我的做法是在合并成功后后端返回一个fileId和fileUrlOA表单提交时把这两个字段埋进隐藏域等表单走审批流程时通过fileId关联到具体附件。这里要注意一个安全问题视频文件可能涉及内部信息OA系统必须做权限校验。上传接口和合并接口我全部加了登录拦截器并校验当前用户是否有对应模块的附件上传权限不能因为做了进度监控就放松鉴权。同时所有视频文件的访问URL都需要带临时签名防止普通用户猜测路径直接下载。5.3 其他值得一试的小技巧我用Redis做进度计数时发现每次分片上传都调用一次increment其实有点浪费网络。后来直接把计数更新放在分片表写入的同一个事务里在数据库更新成功后再异步发送一条Redis消息进度接口兜底查库。这样Redis挂了也能保证进度查询走数据库不会出现用户看到进度条卡在90%不动。日志也别忽视。每个分片上传成功的日志我打了md5 chunkIndex size三个字段排查问题时按MD5搜日志第一次上传是哪儿断了、哪个分片没传完一目了然。最后再提醒一句上线前一定要用带宽限速工具模拟弱网环境把上传速度限到200KB/s测一下分片重传和进度校准。别问为什么等你被生产环境的“网络抖动”教育过就明白了。这套方案我落地之后用户满意度确实上来了至少不再有人半夜打电话说视频传不上去。希望这些经验能帮到你有细节问题可以在评论区继续聊。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

COLMAP点云可视化与6D位姿对齐:从二进制解析到Open3D/PCL实战 2026/10/2 21:46:56

COLMAP点云可视化与6D位姿对齐:从二进制解析到Open3D/PCL实战

简介:这是一款面向三维重建与计算机视觉开发者的点云可视化工具,主要解决Colmap重建结果、pcd/ply点云以及6D位姿R|t难以直观查看的问题。工具支持加载Colmap输出的images、cameras、points3D、project四类文件,也兼容pcd、ply格式点云&#…

阅读更多 →
Splay树原理与C++实现:旋转操作、区间翻转及均摊复杂度详解 2026/10/2 21:46:46

Splay树原理与C++实现:旋转操作、区间翻转及均摊复杂度详解

这会儿想说点关于数据结构的硬核内容。Splay 树(伸展树)你可能听说过,也可能在平衡树的题解里见过它的大名,但始终没动手写过。这玩意在 C 竞赛、数据结构实验、甚至某些工程场景里都挺能打,因为它的思路跟 AVL 树、红…

阅读更多 →
openrig 配置指南:统一管理 Claude Code 与 Codex 的 AI 编码助手运行环境 2026/10/2 21:46:45

openrig 配置指南:统一管理 Claude Code 与 Codex 的 AI 编码助手运行环境

1. openrig 到底想解决什么问题第一次看到 openrig 这个名字,我下意识把它和一堆“AI 命令行工具”联系到了一起。原因很简单,最近围绕 Claude Code、Codex 这类终端智能助手的讨论实在太多,而 openrig 恰好出现在同一批热搜词里。但真正把玩…

阅读更多 →
懂车帝反爬实战:从静态解析到动态行为模拟 2026/10/2 21:46:44

懂车帝反爬实战:从静态解析到动态行为模拟

1. 这不是“又一个爬虫教程”,而是懂车帝数据获取的实战切片懂车帝,这个国内汽车垂直领域流量最大的平台之一,它的车型页结构清晰、数据维度丰富——官方指导价、配置表、实拍图、用户口碑、油耗实测、保值率曲线,甚至还有4S店报价…

阅读更多 →
MySQL+Flask+ECharts:数据可视化全链路实战 2026/10/2 21:46:42

MySQL+Flask+ECharts:数据可视化全链路实战

数据可视化这两年几乎是个人开发者和企业报表岗的必备技能,但很多人在第一步就卡住了:数据在哪?怎么组织?怎么高效地把它变成前端能直接“画”的数据?我实操下来最顺手的组合,不是直接用Excel硬刚&#xff…

阅读更多 →
IT、TT、TN系统详解:低压配电接地方式与选型实操指南 2026/10/2 21:46:33

IT、TT、TN系统详解:低压配电接地方式与选型实操指南

搞电气的人,十有八九都被 IT、TT、TN 这套字母组合绕晕过。我刚入行的时候,在工地上画低压配电图,老师傅随口问一句“你这个回路用的什么系统”,我当场愣住,答不上来。后来自己翻设计手册、跑现场、拆故障记录&#xf…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉