ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于JSP+Servlet的视频文件夹跨平台分块续传上传方案

基于JSP+Servlet的视频文件夹跨平台分块续传上传方案 前段时间给公司内部做了一套视频课程管理后台运维把几百GB的视频素材放在NAS上业务部门的人在Windows和Mac上都有经常需要把某个系列课程的视频文件夹从本地上传到服务器。一开始用的就是普通的表单POST上传结果反馈接二连三传两个G的视频跑到一半断了整个重来上传超出一定大小直接被Tomcat按异常处理文件夹里几十个文件没法一次性选目录结构全乱。后来我用Java最传统的JSPServlet组合做了一套视频文件夹跨平台分块续传方案把这些问题都解决了。这篇文章就把方案的完整设计、关键代码和踩坑记录分享出来适合正在做Web端大文件上传、或者想理解分块续传底层原理的Java开发者参考。1. 方案整体设计与核心思路1.1 为什么视频文件夹上传必须分块续传先说个大前提视频文件普遍不小。单集课程视频动辄几百MB到1GB以上一套系列课程就是几十个G。如果走普通POST表单上传会遇到三层问题。第一层是协议和容器限制。HTTP请求没有一个明确的包大小上限但是Tomcat等Servlet容器对请求体有最大接收限制虽然可以通过maxPostSize调整但把单个请求放大到几个GB本身就是灾难。浏览器端长时间保持一个上传连接中间只要网络抖动一下TCP连接断开这个请求就废了客户端拿不到明确的错误用户只能重来。第二层是服务器内存和中间件风险。Servlet接收上传时如果用request.getInputStream()然后一次性read进byte[]几个GB的文件直接OutOfMemoryError。即使用Part.write()落临时文件一个大请求占用的临时磁盘空间和IO吞吐也会把单机部署的服务拖垮。第三层是业务形态问题。用户传的不是单个文件而是视频文件夹文件夹里有子目录、有视频文件、有字幕文件、还有封面图。普通文件上传控件只能选文件选不了文件夹更保不住目录结构。就算强行用多选文件上传后端的存储路径也只能是平铺业务上根本不可用。分块续传恰好把这三层问题一起解决把大文件切成固定大小的小分片每个分片是独立的小请求就算某一片失败只要记录好哪些分片已成功重新请求的时候跳过已传分片继续传剩下的就行。文件夹结构通过前端遍历File对象时记录相对路径后端按相对路径重建目录跨平台问题交给标准库的Path和File.separator处理。1.2 技术选型为什么还是JSPServlet可能会有朋友问都什么年代了怎么不用Spring Boot这套方案最初是在一个老项目里落地的部署环境是Tomcat 8.5 JDK 8整个项目就是JSPServlet的经典结构没有引入Maven依赖管理甚至还是打war包手动扔进webapps的运维方式。在这种环境里硬塞一个Spring Boot重构改动成本高、上线风险大完全不划算。JSPServlet在这个场景下并没有那么落伍。Servlet本身就是Java Web的底层规范所有Java后端框架最终都要跑在Servlet API之上。用Servlet写接口对请求生命周期、线程模型、流式读取的控制反而更直接不需要理解Spring MVC那层封装。JSP负责渲染上传页面和任务列表EL表达式和JSTL用起来也不复杂页面不追求高交互服务端渲染完全够用。后面如果项目要升级Spring Boot这套Servlet逻辑直接复制到Controller层几乎不用改核心代码迁移成本很低。说白了选型不是看技术新旧是看约束条件。如果你的项目也是传统war包部署、低版本JDK环境、不想引入框架依赖或者你正在回答基于Servlet如何实现分块上传这类面试题这套方案就是最合适的参考。1.3 整体流程与核心模块划分整个上传流程可以概括成六个步骤文字描述比画图更好理解用户在网页上点击按钮选择整个视频文件夹浏览器通过webkitdirectory属性返回文件列表。前端遍历文件列表对每个文件生成唯一的fileKey记录它的相对路径、大小、最后修改时间。前端按固定大小比如5MB把文件切割成多个分片先调用查询接口拿到这个文件已上传的分片索引集合。对每个未上传的分片发起上传请求后端把分片以chunk_序号.part的形式写入临时目录。所有分片上传完成后前端发起合并请求后端按分片序号有序合并写入最终的目标目录同时清理临时分片。整个文件夹内所有文件都完成合并后前端提示上传完成。后端按职责拆成三个ServletUploadServlet负责接收单个分片并落盘CheckServlet负责查询某文件已上传的分片索引MergeServlet负责分片合并和临时文件清理。加上一个渲染上传页面的upload.jsp整个后端就这四个核心文件没有复杂的配置。任务进度不依赖数据库而是直接扫描临时目录里的分片文件名。这样设计有个好处服务端进程重启后只要临时目录还在前端重新调用查询接口依然能拿到历史进度接着续传。对上传这种长耗时任务来说这个特性非常实用。2. 前端分片实现与核心细节2.1 用webkitdirectory读取文件夹网页端选择文件夹主流浏览器的做法是利用input标签的webkitdirectory属性。虽然这个属性从前缀看是WebKit私有属性但Chrome、Edge、Firefox目前都支持Safari新版也基本可用实测足够覆盖主流场景。input typefile idfolderPicker webkitdirectory multiple /加上multiple是为了兼容某些浏览器在文件夹模式下仍然要求多文件属性。用户选择完文件夹后这个input.files返回的不是文件夹对象而是文件夹内所有文件的扁平列表。每个File对象上有一个webkitRelativePath属性保存了文件相对于所选根目录的路径比如Linux基础课程/第01章/视频01.mp4。遍历并收集文件信息const tasks []; for (const file of e.target.files) { if (!file.webkitRelativePath) continue; const task { relativePath: file.webkitRelativePath, fileName: file.name, fileSize: file.size, lastModified: file.lastModified }; tasks.push(task); } // 按相对路径排序方便后面顺序上传 tasks.sort((a, b) a.relativePath.localeCompare(b.relativePath));这里的relativePath就是后面重建目录结构的关键。用户可能拖进来的文件夹里混着Thumbs.db、.DS_Store这类系统文件前端可以做个过滤只保留视频扩展名和字幕文件减少没意义的传输。2.2 分片切割与fileKey生成方案浏览器端用File.slice(start, end)可以切出一个BlobBlob就是一个独立的二进制分片可以直接放进FormData提交。分片大小我推荐选5MB这是一个平衡点分片太小时请求数量过多成千上万个HTTP请求光握手开销就很大分片太大又失去了断点续传的精度比如2GB文件用50MB一片断一次可能要重传50MB。const CHUNK_SIZE 5 * 1024 * 1024; // 5MB function splitFile(file) { const totalChunks Math.ceil(file.size / CHUNK_SIZE); const chunks []; for (let i 0; i totalChunks; i) { const start i * CHUNK_SIZE; const end Math.min(start CHUNK_SIZE, file.size); chunks.push({ index: i, blob: file.slice(start, end) }); } return chunks; }接下来说fileKey。它用来唯一标识一个文件后端靠它创建独立临时目录。最简单的方案是用相对路径文件大小最后修改时间拼接后做一次哈希比如用SparkMD5处理这个字符串或者直接encodeURIComponent之后当key用。这里有个坑要提醒不要对大视频文件做全量MD5。很多人受网盘分块上传思路影响上来就给整个文件算MD5算完再做内容校验。视频文件动辄1GB以上浏览器主线程全量计算MD5可能需要几十秒甚至几分钟期间页面假死用户完全无法操作。真需要内容强校验就分片级MD5每个分片独立计算代价可控。我这套方案为了性能fileKey直接用btoa(unescape(encodeURIComponent(relativePath fileSize lastModified)))生成兼顾唯一性和长度控制。2.3 断点判定、并发控制与进度反馈每个文件上传前先请求CheckServlet带上fileKey拿到服务端已经收到的分片索引集合async function getUploadedChunks(fileKey) { const resp await fetch(${basePath}/check?fileKey${encodeURIComponent(fileKey)}); const data await resp.json(); return data.uploadedChunks || []; }拿到已上传索引后前端生成待传分片列表只提交里面对应索引未出现的分片。这就是续传的核心逻辑不用服务端做复杂的状态机只要保证分片写入是幂等的查询接口能给出准确索引续传自然成立。上传分片用fetch发送FormData比较方便async function uploadChunk(task, chunk) { const formData new FormData(); formData.append(fileKey, task.fileKey); formData.append(chunkIndex, chunk.index); formData.append(totalChunks, task.totalChunks); formData.append(fileName, task.fileName); formData.append(relativePath, task.relativePath); formData.append(chunk, chunk.blob, ${task.fileName}.part${chunk.index}); const resp await fetch(${basePath}/upload, { method: POST, body: formData }); if (!resp.ok) throw new Error(chunk ${chunk.index} upload failed); }并发控制也很重要。如果一次性把所有分片全部同时发出去浏览器对同域名并发连接数有限制服务端也会被大量请求打满。一般我控制每个文件同时最多2到3个分片在传多个文件之间串行处理也就是同一时刻只处理一个视频文件这个文件传完再传下一个。这样进度提示从第几个文件的角度来看特别清晰用户也知道还剩多少文件。进度计算有两个维度单文件进度 已成功分片数 / 总分片数总进度 所有文件已传字节数 / 所有文件总字节数。前端维护一个uploadedBytesMap每次分片成功就把该分片字节数累加进去再刷新界面体验上就很直观了。3. 后端三个Servlet设计与代码实现3.1 公共配置与参数约定三个Servlet共用一套约定请求和响应编码全部UTF-8分片接口接收multipart/form-data查询接口用GET合并接口用POST返回统一格式的JSON。响应JSON我一般用一个硬编码的小工具方法不引入JSON库避免老项目里根本没有依赖可用的尴尬protected void writeJson(HttpServletResponse resp, int code, String msg) throws IOException { resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\code\: code ,\msg\:\ msg \}); }上传和合并接口需要声明MultipartConfig。这里几个参数值得认真设WebServlet(/upload) MultipartConfig( fileSizeThreshold 1024 * 1024, maxFileSize 10 * 1024 * 1024, maxRequestSize 50 * 1024 * 1024 ) public class UploadServlet extends HttpServlet { ... }maxFileSize是单个上传文件的大小上限由于我们每个分片固定5MB这里给10MB留冗余。maxRequestSize是单次请求整体大小表单里还有几个字段给到50MB足够了。fileSizeThreshold表示超过1MB的分片直接写临时文件而不是放内存防止小内存机器被并发请求击穿。3.2 UploadServlet分片接收与幂等存储UploadServlet的doPost逻辑很直接从请求体里取出任务信息把分片写入文件。关键点是整个写入过程必须幂等同一个分片上传两次第二次直接覆盖或者跳过不影响最终结果。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String fileKey request.getParameter(fileKey); String chunkIndex request.getParameter(chunkIndex); String fileName request.getParameter(fileName); String relativePath request.getParameter(relativePath); // 校验参数 if (fileKey null || chunkIndex null || fileName null || relativePath null) { writeJson(response, 1, 参数缺失); return; } // 临时目录/data/upload_temp/{fileKey}/ File tempDir new File(getUploadTempRoot(), fileKey); if (!tempDir.exists()) { tempDir.mkdirs(); } File chunkFile new File(tempDir, chunk_ chunkIndex .part); // 分片已存在则直接跳过实现幂等 if (chunkFile.exists() chunkFile.length() 0) { writeJson(response, 0, ok); return; } Part part request.getPart(chunk); try (InputStream in part.getInputStream(); FileOutputStream out new FileOutputStream(chunkFile)) { byte[] buffer new byte[8192]; int len; while ((len in.read(buffer)) ! -1) { out.write(buffer, 0, len); } out.flush(); } writeJson(response, 0, ok); }注意getUploadTempRoot()不要用getServletContext().getRealPath(/)拼目录。Tomcat高版本在某些部署方式下getRealPath可能返回null而且war包重新部署会清空目录导致所有临时分片丢失。正确做法是用一个固定的外部目录比如Linux下的/data/video_upload/tempWindows下的D:/video_upload/temp通过配置项或者环境变量注入这也是跨平台很关键的一点。3.3 CheckServlet续传状态查询CheckServlet实现很简单扫描临时目录下该文件的chunk_*.part文件提取索引号返回给前端。这里提取索引号时要注意文件名格式固定为chunk_0.part、chunk_1.part这种用String截取或者正则都行WebServlet(/check) public class CheckServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String fileKey request.getParameter(fileKey); ListInteger uploadedChunks new ArrayList(); File tempDir new File(getUploadTempRoot(), fileKey); if (tempDir.exists()) { File[] files tempDir.listFiles((dir, name) - name.startsWith(chunk_) name.endsWith(.part)); if (files ! null) { for (File f : files) { String name f.getName(); int index Integer.parseInt(name.substring(6, name.length() - 5)); uploadedChunks.add(index); } } } Collections.sort(uploadedChunks); response.setContentType(application/json;charsetUTF-8); StringBuilder sb new StringBuilder({\code\:0,\uploadedChunks\:[); for (int i 0; i uploadedChunks.size(); i) { if (i 0) sb.append(,); sb.append(uploadedChunks.get(i)); } sb.append(]}); response.getWriter().write(sb.toString()); } }这个接口本质上承担的是断点状态报告的职责。服务端不需要维护任何内存状态重启进程后只要磁盘上的临时分片还在查询结果就是准确的这比用ConcurrentHashMap存任务状态的方案可靠得多。当然如果你有多台服务器负载均衡临时目录必须指向共享存储NFS或者NAS否则前端可能会查询到不一致的分片索引这一点在4.2节再细聊。3.4 MergeServlet分片合并与跨平台路径处理合并是整个方案里最容易写错的地方。两个坑分片顺序不能乱文件路径分隔符不能硬编码。合并之前先把分片文件按索引排序然后用FileChannel做零拷贝传输比一个个read再write高效很多WebServlet(/merge) public class MergeServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String fileKey request.getParameter(fileKey); String fileName request.getParameter(fileName); String relativePath request.getParameter(relativePath); // 防路径穿越相对路径不能包含..或者绝对路径 if (!isPathSafe(relativePath) || !isPathSafe(fileName)) { writeJson(response, 3, 非法路径); return; } File tempDir new File(getUploadTempRoot(), fileKey); File[] chunkFiles tempDir.listFiles((dir, name) - name.startsWith(chunk_) name.endsWith(.part)); if (chunkFiles null || chunkFiles.length 0) { writeJson(response, 4, 分片不存在); return; } // 按索引排序 Arrays.sort(chunkFiles, Comparator.comparingInt(f - Integer.parseInt(f.getName().substring(6, f.getName().length() - 5)))); // 确认分片齐全缺失的分片索引补0 SetInteger indexSet new HashSet(); for (File f : chunkFiles) { int idx Integer.parseInt(f.getName().substring(6, f.getName().length() - 5)); indexSet.add(idx); } int expectedChunks Integer.parseInt(request.getParameter(totalChunks)); for (int i 0; i expectedChunks; i) { if (!indexSet.contains(i)) { writeJson(response, 5, 缺少分片: i); return; } } // 用 Path 构建目标路径跨平台安全 Path targetPath Paths.get(getUploadRealRoot(), relativePath); File targetFile targetPath.toFile(); File parentDir targetFile.getParentFile(); if (!parentDir.exists()) { parentDir.mkdirs(); } try (FileOutputStream fos new FileOutputStream(targetFile); FileChannel outChannel fos.getChannel()) { for (File chunk : chunkFiles) { try (FileInputStream fis new FileInputStream(chunk); FileChannel inChannel fis.getChannel()) { inChannel.transferTo(0, inChannel.size(), outChannel); } } } // 合并成功后清理临时目录 deleteDir(tempDir); writeJson(response, 0, ok); } private boolean isPathSafe(String path) { if (path null || path.isEmpty()) return false; // 拒绝绝对路径和..穿越 return !path.startsWith(/) !path.startsWith(\\) !path.matches(^[A-Za-z]:.*) !path.contains(..); } }isPathSafe这层校验必须有因为relativePath是从前端传上来的攻击者完全可以伪造一个../../etc/passwd之类的路径如果直接new File(relativePath)拼接就可能实现路径穿越。虽然这个接口在企业内网使用风险相对低但这个习惯要养成任何客户端传上来的路径都必须经过白名单校验。4. 完整断点续传流程与异常恢复4.1 一次完整的断网续传演示以一个真实场景为例用户上传Linux入门课程文件夹里面有第01章/视频01.mp4和第01章/笔记.pdf。前端遍历后生成两个任务先处理视频01.mp4大小是1.2GB按5MB分片切成246片。前100片上传顺利界面进度显示视频01.mp4 100/246 已完成 40%。此时办公网络突然断开上传请求抛异常前端捕获到后立即停止当前文件的后续分片提交把已成功的分片索引保存在本地localStorage里同时把任务状态标记为待恢复。网络恢复后用户不一定要重新选择文件夹来做续传也更不应该让他再选一次。页面里有一个未完成任务列表这个列表由JSP在页面加载时通过CheckServlet扫描临时目录后渲染出来。用户点击继续上传前端拿到fileKey调用一次check接口获得服务端实际存在的分片集合[0..100]然后从101片继续上传。这里有个细节值得说明如果用户在断网期间手动清理了浏览器缓存本地保存的进度丢了前端应该以服务端查询结果为准而不是以本地记录为准。本质原因在于服务端临时目录才是分片状态的唯一事实来源客户端进度记录只是展示用的缓存。4.2 服务器重启与临时文件清理策略分块续传的时间跨度可能很长一个几百GB的文件夹传十几个小时很正常期间服务端Tomcat可能因为发布新版本而重启或者干脆宕机。重启之后CheckServlet仍然能从磁盘临时目录读到所有已上传的分片所以客户端续传不受影响。真正要小心的是Tomcat重启时清空了临时目录或者临时目录被系统清理进程当成垃圾删掉了。所以两个硬性要求第一临时目录必须配置在war包之外不能依赖getRealPath第二临时目录要有自己的清理策略。这里我见过不少项目是写一个TimerTask定时清理超过7天未更新的临时文件配合web.xml里配置context-param保存过期天数用Java最朴素的定时任务就够不需要引入Quartz。清理逻辑很简单扫一遍upload_temp下的子目录比较最后修改时间超过阈值整个目录删掉。这样既能防止磁盘被半途而废的任务塞满又能给用户留出足够的续传时间窗口。还有一个容易被忽略的点合并请求发出后如果服务端合并到一半进程挂了临时目录里的分片还没清理而最终目录里已经写入了半个文件。重新发起合并时会发现文件名冲突或者文件不完整。我的做法是合并时先写一个临时文件名比如视频01.mp4.merging全部写完再renameTo成最终文件名同时把文件完整的判定交给分片齐全校验和文件大小比对规避了这种写一半的脏状态。4.3 安全防护与文件校验要点文件上传永远是最容易出安全问题的接口分块上传等于把上传入口拆成了多个接口反而扩大了攻击面。除了前面说的路径穿越校验我还会做三层防护第一层是文件类型白名单。视频文件夹里允许的扩展名是.mp4 .mkv .avi .mov .flv .wmv .ts .pdf .ass .srt .jpg .png前端选择时过滤一次后端合并时严格再校验一次扩展名不在白名单里直接返回错误。注意光看Content-Type不可靠浏览器上传时这个字段完全由客户端控制真正稳妥的是后端检查扩展名和文件头魔数视频文件的头几个字节特征比较固定简单校验可以显著提高安全性。第二层是上传配额限制。用一个大ConcurrentHashMap维护每个sessionId当前正在上传的总字节数超过配额比如单用户50GB就拒绝新的上传请求。虽然断点续传场景下用户会分多次请求传输一个文件但总量配额仍然必要防止有人恶意给临时目录塞垃圾数据。第三层是接收分片时的完整性校验。分片上传接口返回的JSON里带一个receivedSize字段前端拿到后和本地Blob的size比对不一致就重传这个分片。这个校验成本极低但能挡住传输过程中偶发的数据截断问题。5. 常见问题排查与性能优化5.1 高频问题速查表我在实际部署和联调过程中收集了几个高频故障整理成表格遇到问题先对照自查现象常见原因解决办法上传分片报413 Request Entity Too LargeTomcat默认单请求大小限制或MultipartConfig的maxRequestSize太小调大MultipartConfig参数检查Tomcat的maxSwallowSize配置页面上传进度到99%后卡住最后一个分片超过maxFileSize限制或者合并请求未发出确认分片切割边界计算正确end Math.min(start CHUNK_SIZE, file.size)别写错中文文件名保存乱码前端没有对fileName做encode后端setCharacterEncoding(UTF-8)位置不对前端上传参数用encodeURIComponent后端统一在doPost第一行设置UTF-8Linux下目标目录创建失败web应用对/data/video_upload没有写权限检查目录属主chown -R tomcat:tomcat /data/video_upload合并后的视频文件播放卡顿或损坏分片排序错误或者合并时用了文本模式写入分片必须按索引升序排序用FileChannel.transferTo做字节级复制不能用FileWriterTomcat重启后查询不到历史分片临时目录配置在war包内部被重新部署清空临时目录配置到外部绝对路径配合环境变量注入JSP页面刷新后列表消失任务列表是页面加载时通过JS获取的刷新未重新渲染在JSP的script代码块中页面就绪后调用check接口拉取未完成任务渲染列表5.2 服务端性能优化与合并效率分片上传本身会把一个大请求拆成多个小请求服务端压力从单连接大吞吐变成多连接并发小吞吐这两者的性能特征完全不同。我实测下来客户端并发3个分片上传时Tomcat默认线程池处理能力足够不会出现线程饥饿超过5个并发后瓶颈开始在磁盘IO而不是CPU合并时尤其明显。合并阶段的优化空间最大。如果临时目录和目标目录在同一个磁盘分区FileChannel.transferTo走的是内核态零拷贝路径合并一个1GB的文件只需要几秒钟。如果临时目录和目标目录跨磁盘传输效率会下降但比起用byte[]循环读写仍然快很多。还有一个轮转实测的经验把分片大小从5MB调整到10MB分片数量减半合并效率提升约15%但因为分片粒度变大断点续传精度会下降。单文件小于100MB的项目建议直接不分片按小文件上传就行没必要为小文件付出分片合并的复杂度。并发上传大文件夹时JSP页面上的JS运行在浏览器主线程里大量fetch回调会让UI卡顿。可以把进度计算和队列调度逻辑放到Web Worker里页面只负责渲染进度条这样用户在上传大文件时还能继续浏览页面、滚动查看历史任务体验好很多。这个改造不复杂但收益明显属于投入产出比很高的优化项。5.3 后续演进方向与Spring Boot迁移这套JSPServlet方案解决的是单机部署、传统war包环境下的问题。如果后面要扩展有两个方向值得提前考虑。方向一是模块化演进把三个Servlet先拆成Service层UploadService、CheckService、MergeServiceServlet只做参数解析和响应封装。这个改动不需要改前端但后续如果要换成Spring Boot直接把Servlet里的业务逻辑平移到Service和Controller里几乎不用重写业务代码。MultipartConfig对应的注解在Spring MVC里是MultipartFile而FileChannel.transferTo的合并逻辑完全通用这部分是迁移成本最低的代码。方向二是多实例与对象存储当上传量增长到单台服务器磁盘撑不住时需要把临时目录和最终存储迁移到共享NAS或者对象存储OSS/S3/MinIO。迁移后CheckServlet扫描的不再是本地目录而是对象存储的listObjects接口MergeServlet的分片合并逻辑要改成服务端合并或者用对象存储的multipart upload能力。分片上传协议本身不需要太大改动前端fileKey、分片索引、查询已上传集合这几个语义在对象存储里依然成立这是当初设计时把协议定义得足够干净带来的红利。5.4 关于定时任务清理与磁盘监控最后单独提一下临时目录的磁盘监控。分块续传方案最怕的不是程序bug是磁盘被写满以后连重启都做不到。临时目录和最终存储目录必须挂载到有容量监控的分区我会在运维脚本里加一个每日检查du -sh /data/video_upload/temp超过设定阈值比如50GB自动清理最久未更新的任务目录同时发告警通知。清理策略和业务要商量好上传中的任务被误删了用户会非常崩溃所以我在清理脚本里额外加了个保护只清理最后修改时间超过72小时的目录并且清理前先输出日志方便回溯。前端也可以做个配合上传页面顶部显示服务端磁盘剩余空间空间低于10%直接禁用新上传提示用户联系管理员。这些细节不在核心代码里但真正上线后它们比上传逻辑本身更容易决定这个系统能不能长期稳定跑下去。最后的个人体会我在实际动手写这套方案之前在几个关键决策上反复犹豫过要不要用数据库记录分片状态、要不要引入Spring Boot重写、要不要给每个分片做MD5校验。最后都因为引入额外复杂度是否真的能解决问题这个标准被否掉了。扫描临时目录作为状态来源、Servlet直接处理流式写入、前端并发控制靠一个计数器这些简单的做法在落地后表现非常稳维护成本也低。最后再分享一个小技巧前端在分片上传失败时不要立即重试先等1秒、然后2秒、4秒这样指数退避最多重试3次。单分片失败往往是因为瞬时的网络波动重试太频繁反而会把波动放大成服务端压力。这个小改动几乎零成本但能把断点续传的成功率从90%提到99%以上很值得做。后续如果有人想在这个方案上继续扩展建议先把fileKey的生成从路径大小修改时间升级成真正的分片级MD5校验这样能发现极小概率的静默数据损坏。但前提是先解决性能问题把MD5计算放到Web Worker里跑。希望这篇分享能帮你少踩几个坑如果你在实现过程中遇到什么奇怪的问题欢迎按文章里的排查表对照一遍大多数问题都能对上号。
返回列表