☰
大文件上传实战:Java Spring Boot分片与断点续传方案
2026/10/2 2:56:55 网站建设 项目流程

写大文件上传这块,基本是每个做 Web 开发的 Java 工程师迟早都要碰的硬骨头。我自己第一次接手这个需求是在一个企业网盘项目里,用户要传单个 1.5GB 的设计源文件,一开始图省事直接用 MultipartFile 一把梭,结果线上服务直接把 JVM 堆内存吃爆,OOM 了好几次。后来老老实实把分片上传和断点续传整套方案重新设计了一遍,才算彻底解决问题。今天就把这套经过实战验证的方案完完整整拆开讲,从设计思路到前后端核心代码,再到我踩过的各种坑,争取让不同基础的读者看完都能在自己项目里落地。

这套方案适合什么场景呢?后端是 Java 技术栈,框架用的是 Spring Boot,前端是原生 JS 或者 Vue、React 这类单页应用,需要处理动辄几百 MB、甚至几个 GB 的文件上传。做完这套方案,用户传大文件时不再担心网络波动,中断了也只需要点一下继续,不用从头再传一遍。接下来我按自己的实现路径一步步说。

1. 先把问题拆清楚:大文件上传到底难在哪

1.1 大文件上传的三大痛点

做需求之前,一定要先把问题具象化。大文件上传用传统方式处理,通常会碰到三个绕不开的坎。

第一是整个文件数据放进一次 HTTP 请求,网络稍微不稳定就全盘失败。一个 2GB 的文件,就算内网带宽 100Mbps,也要将近三分钟才能传完。这期间只要断一次网,或者代理服务器超时,整个请求就废了,用户只能从头再来。而且很多网关、负载均衡器对请求体大小和传输时长都有硬性限制,比如 Nginx 默认的client_max_body_size是 1MB,不配置基本传什么都费劲。

第二是服务器内存压力。Spring MVC 处理MultipartFile时,会把请求体数据临时写入磁盘临时目录,但解析过程中文件流经过内存,框架的spring.servlet.multipart.max-request-size和max-file-size默认只有 1MB 和 10MB。你把限制调大,意味着并发一高,内存和临时磁盘的消耗就非常夸张。我之前在生产环境调过 100MB 的限制,结果 50 个用户同时传文件,磁盘 IO 直接打满,数据库连接也跟着被拖垮。

第三是缺乏进度粒度和可恢复性。浏览器原生表单上传只能显示"正在上传"转圈,用户完全不知道传了多少、还剩多少。一旦失败,没有任何机制告诉服务端"我已经传过一部分",从零重来是唯一选择。这就是完整上传与分片上传的核心区别:完整上传是把文件看成一个不可分割的整体,要么全成,要么全废;分片上传则把文件切成了若干个独立单元,每个单元可以单独成功、单独重试。

1.2 分片上传和断点续传的关系

很多人一开始容易把这两个概念混在一起,其实它们是两个层面的东西。分片上传是传输策略,断点续传是应用在该策略上的恢复能力。分片负责把大文件切成小块,每块独立传输,降低单次请求的失败半径;断点续传则依赖分片上传产出的元数据记录,服务端能告诉你"哪些分片已经到了,哪些还没到",前端只补传缺失的那部分就行。

这里得强调一点,断点续传不等于续传已经损坏的分片。实际工程中,我们为了稳妥,会对已上传的分片做尺寸校验甚至 MD5 校验,如果发现某个分片大小不对或者内容哈希不匹配,就要求前端重新传这个分片。也就是说,断点续传的"断点"是建立在分片级完整性校验基础上的,不是说我传过就说传过。

记住这个逻辑后,整个方案就清晰了:前端对文件切片并编号,上传前先问服务端该文件的哪些分片已存在,然后并行上传缺失分片,全部完成后通知服务端合并。下面我把每个环节展开讲。

2. 方案选型:前后端技术栈怎么定

2.1 前端方案:File.slice 切片与并发控制

前端切片本质上是利用浏览器提供的File.slice()方法,按固定大小把文件分成多个 Blob 片段。这个 API 兼容性很好,现代浏览器都支持,不需要引入额外插件。

切片的大小怎么定,我自己的经验是 2MB 到 10MB 之间比较合适。切太小,比如 512KB,虽然单片失败重传的成本极低,但分片数量会爆炸——1GB 文件要切 2048 片,每一片都要发起一次 HTTP 请求,请求数和请求头开销会拖慢整体速度。切太大,比如 50MB,分片数量少了,但单次请求失败重传的代价又上去了,同时前端同步读大块数据也会消耗更多内存。我个人偏好 5MB 这个折中值,兼顾数量、内存和失败成本。

并发数也要控制。HTTP/1.1 下浏览器对同一个域名有 6 个左右的连接限制,你一次发 20 个请求,剩下的只能排队,反而容易造成 TCP 连接拥堵。我通常在优化阶段写一个简单的并发调度器,把并发数控制在 3 到 5 个,配合进度事件做整体展示。代码结构上,我用一个队列收集所有分片,启动固定数量的 worker 从队列里取任务,每个 worker 负责一个分片的上传,传完自动取下一条。这套思路和线程池是同一个道理,非常好用。

2.2 后端方案:Spring Boot 加本地磁盘存储

后端我推荐直接用 Spring Boot,它内置的 Multipart 解析机制配合自定义 Controller 就能处理分片接收,不需要额外引入重型中间件。存储这块,分片阶段和合并阶段的策略不一样。分片接收后先写入磁盘上的临时分片目录,目录结构按照文件标识组织,例如/data/upload_temp/{identifier}/{chunkNumber}.part,每个分片是一个独立的小文件。合并完成后,再把最终文件移到正式存储目录。

为什么不直接用 Redis 或者数据库存分片二进制?原因很简单:文件数据是典型的流式大对象,放内存型存储太浪费,放数据库会导致行大小爆炸,查询和备份都会出问题。文件系统才是分片最自然、最廉价的载体。元数据倒可以放进数据库,但要控制好字段粒度,不要一个分片一条记录,那样数据量太大。我的习惯是:文件信息表只在合并阶段写入一条记录(文件标识、文件名、总分片数、最终大小、存储路径),分片上传状态用目录下的小文件存在性来判断。

这里还要考虑生产环境的 Nginx 配置。如果你前面有 Nginx 反代,一定记得把client_max_body_size调到比分片大小更大,同时把proxy_read_timeout调长,比如 300 秒。不然前端辛辛苦苦切好的分片,在 Nginx 这一层就被拦截了,后面的代码再优秀也没用。

2.3 文件标识:MD5 到底该不该算

断点续传必须解决一个问题:怎么识别"同一个文件"。我见过最简单的方案是用文件名加文件大小拼一个标识,但两个用户传不同内容、却同名的文件就会撞车。正经做法是前端算文件的 MD5,把 32 位十六进制字符串当作文件唯一标识,传给服务端做分片目录和状态查询的 key。

但这里有个现实问题:计算一个 2GB 文件的 MD5,浏览器里通常要几秒甚至十几秒,而且主线程会被阻塞,页面直接卡住。解决方案有几个。一个是用 Web Worker 放到后台线程算,算完再弹进度。另一个是分片算归分片算,MD5 用"抽样计算"的方式——取文件头部 2MB、中间若干块、尾部 2MB 拼接起来算一个简化哈希,速度极快,能过滤掉绝大多数不同文件,算是一种工程妥协。我个人更推荐方案一,如果用户设备性能允许,就走完整 MD5;如果想快,就在交互上提示"正在验证文件"。服务端合并后同样要对最终文件做一次 MD5 校验,与前端提交的 MD5 进行比对,这是数据一致性检查的最后一环。

3. 完整实现步骤:从零搭一个可落地的分片上传

3.1 前端实现:切片、查询状态、上传与进度

先看前端核心代码。下面我用原生 JavaScript 加 XMLHttpRequest 来写,方便你理解底层逻辑,在 Vue 或 React 项目中换成 axios 也是同样的思路。

const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB function createFileChunks(file) { const chunks = []; let start = 0; while (start < file.size) { const end = Math.min(start + CHUNK_SIZE, file.size); chunks.push(file.slice(start, end)); start = end; } return chunks; } function getFileIdentifier(file, md5) { return md5; // 由 spark-md5 等库在前台计算得出 } async function checkUploadStatus(identifier) { const resp = await fetch(`/upload/check?identifier=${identifier}`); const data = await resp.json(); return data.uploadedChunks || []; // 返回已上传分片序号数组 }

真正上传分片的函数要注意一个细节:每个分片请求都要带上identifier、chunkNumber、totalChunks三个参数,而且分片的二进制数据要通过FormData以multipart/form-data格式提交,后端才能用MultipartFile接收。

function uploadChunk(fileChunk, identifier, chunkNumber, totalChunks, md5) { const formData = new FormData(); formData.append('file', fileChunk); formData.append('identifier', identifier); formData.append('chunkNumber', chunkNumber); formData.append('totalChunks', totalChunks); formData.append('md5', md5); return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', '/upload/chunk'); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { // 可以通过回调把单分片进度抛给 UI 层 } }; xhr.onload = () => (xhr.status === 200 ? resolve() : reject(new Error(`chunk ${chunkNumber} upload failed`))); xhr.onerror = () => reject(new Error('network error')); xhr.send(formData); }); }

断点续传的关键在调度逻辑。用户点击上传后,先调用checkUploadStatus拿到已上传分片集合,过滤掉这些分片,再对剩余分片启动并发上传。这个"先询问、再补传"的流程,就是断点续传在前端层面的完整闭环。

async function uploadFile(file, md5) { const chunks = createFileChunks(file); const identifier = getFileIdentifier(file, md5); const uploaded = await checkUploadStatus(identifier); const uploadedSet = new Set(uploaded); const pending = chunks .map((chunk, index) => ({ chunk, index: index + 1 })) .filter(({ index }) => !uploadedSet.has(index)); await runConcurrentUpload(pending, file, identifier, md5); await fetch('/upload/merge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ identifier, fileName: file.name, totalChunks: chunks.length }) }); }

runConcurrentUpload是并发控制器,我用滑动窗口的方式实现:维护一个活跃任务集合,最大 4 个,某个分片结束就从待上传队列里补充一个新任务进来,直到全部完成。

3.2 后端实现:接收分片与元数据管理

后端第一个接口是接收分片的POST /upload/chunk。参数里除了 MultipartFile,还必须包含文件标识、分片序号和总分片数。分片序号和总分片数是前端切片信息的直接映照,服务端保存分片文件时要把它们写到文件名里,这样合并阶段才能知道顺序。

@RestController @RequestMapping("/upload") @Slf4j public class UploadController { @Value("${upload.temp-dir:/data/upload_temp}") private String tempDir; @Value("${upload.storage-dir:/data/upload_storage}") private String storageDir; @PostMapping("/chunk") public Result uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("identifier") String identifier, @RequestParam("chunkNumber") Integer chunkNumber, @RequestParam("totalChunks") Integer totalChunks, @RequestParam("size") Long chunkSize) throws IOException { if (file.isEmpty()) { return Result.error("分片内容为空"); } // 校验分片大小,防止客户端传入与声明不符的数据 if (file.getSize() != chunkSize) { return Result.error("分片大小不匹配"); } Path chunkDir = Paths.get(tempDir, identifier); Files.createDirectories(chunkDir); Path chunkFile = chunkDir.resolve(chunkNumber + ".part"); // 已存在的分片直接跳过,实现幂等 if (Files.exists(chunkFile) && Files.size(chunkFile) == file.getSize()) { return Result.success("分片已存在"); } try (InputStream in = file.getInputStream()) { Files.copy(in, chunkFile, StandardCopyOption.REPLACE_EXISTING); } return Result.success("分片上传成功"); } }

这个接口做了三个关键设计。第一,基于文件名加序号的组织方式,让断点续传的状态查询变得非常简单——查询目录下有哪些文件,就等价于查询哪些分片已上传。第二,先判断"文件存在且大小一致",这是幂等处理,前端重复上传同一个分片时不会重复写磁盘。第三,用Files.createDirectories创建目录,并发情况下也是安全的,不会因为目录不存在而报错。

状态查询接口也顺手实现。前端要拿到已上传分片列表,后端只需要遍历目录:

@GetMapping("/check") public Result checkUpload(@RequestParam("identifier") String identifier) throws IOException { Path chunkDir = Paths.get(tempDir, identifier); if (!Files.exists(chunkDir)) { return Result.success(Collections.emptyList()); } List<Integer> uploadedChunks; try (Stream<Path> stream = Files.list(chunkDir)) { uploadedChunks = stream .map(path -> path.getFileName().toString()) .filter(name -> name.endsWith(".part")) .map(name -> name.substring(0, name.indexOf('.'))) .map(Integer::parseInt) .sorted() .collect(Collectors.toList()); } return Result.success(uploadedChunks); }

注意这里返回的是有序的序号列表,前端可以直接转成 Set。如果用户传到一半关掉浏览器,下次打开同一个文件重新上传,这个接口返回的就是上个会话的成果,续传就在这一步完成了。

3.3 合并分片:RandomAccessFile 的正确用法

合并是后端最容易出错的一环。常见的错误做法是遍历分片后用FileOutputStream逐个追加写入,一旦分片到达顺序不固定,写出来的文件就是乱的。正确的做法有两种:一种是把所有分片按序号排好,顺序追加;另一种是使用RandomAccessFile按分片序号计算文件偏移量,随机写入对应位置。我推荐第二种,原因很简单:它可以完全不依赖分片到达顺序,只要所有分片都到齐,就能按偏移量拼出完整文件。

@PostMapping("/merge") public Result merge(@RequestBody MergeRequest request) throws IOException { String identifier = request.getIdentifier(); String fileName = request.getFileName(); int totalChunks = request.getTotalChunks(); Path chunkDir = Paths.get(tempDir, identifier); Path targetFile = Paths.get(storageDir, identifier + "_" + fileName); long totalSize = 0L; for (int i = 1; i <= totalChunks; i++) { Path part = chunkDir.resolve(i + ".part"); if (!Files.exists(part)) { return Result.error("分片缺失,无法合并,缺失序号: " + i); } totalSize += Files.size(part); } try (RandomAccessFile raf = new RandomAccessFile(targetFile.toFile(), "rw")) { raf.setLength(totalSize); byte[] buffer = new byte[8 * 1024]; for (int i = 1; i <= totalChunks; i++) { Path part = chunkDir.resolve(i + ".part"); long offset = (long) (i - 1) * CHUNK_SIZE; // 分片大小固定,偏移量直接计算 try (InputStream in = Files.newInputStream(part)) { raf.seek(offset); int len; while ((len = in.read(buffer)) != -1) { raf.write(buffer, 0, len); } } } } // 合并完成后清理分片目录 deleteRecursively(chunkDir); return Result.success("合并成功", targetFile.toString()); }

这里有个最关键的细节:CHUNK_SIZE必须是前端切片时的同一个值,而且除了最后一片,前面每片大小都要严格等于它。否则按(i - 1) * CHUNK_SIZE计算的偏移量就是错的,写出来的文件会错位。我在 service 层会把前端上传的所有分片大小先做一次校验,遇到与预期不一致的分片直接返回错误,宁可不合并也不能生成损坏文件。

还有一个容易被忽略的点:合并阶段不要用"把整个文件读到内存再写出去"的方式,一定要用缓冲流按块读写。8KB 的缓冲区对机械硬盘和 SSD 都比较友好,内存占用极小,即使合并几个 GB 的文件也不会 OOM。

3.4 断点续传的完整交互闭环

把前端和后端的逻辑串起来看,断点续传的完整交互是这样的:用户选择文件后,前端先计算 MD5,然后立刻调用/upload/check查询历史状态。如果文件从没上传过,返回空列表,前端从第 1 片开始传。如果上传过一部分,返回已上传分片序号,前端跳过这些分片只传缺失的。传完最后一分片,调用/upload/merge等待合并结果。

这个流程里前后端各有一个幂等设计。前端是"跳过已上传分片",后端是"分片已存在则直接返回成功"。两者配合,即使前端状态查询结果滞后几毫秒,造成重复上传某个分片,后端也不会写两次,保证了数据不会在分片阶段错乱。

关于"秒传"这个衍生功能,实现起来就一句话:前端算完 MD5 后调检查接口时,如果后端发现这个文件已经存在且完整,就直接返回"文件已存在,无需上传",前端提示用户秒传成功。这个功能在企业网盘和云盘产品里很常见,本质就是断点续传状态检查的逆向利用。

4. 踩坑实录:我在实战中遇到的典型问题

4.1 并发上传导致的分片错乱与重复

第一次上线并发上传后,我发现日志里偶尔出现同一个分片被后端处理两遍的情况。原因是前端并发控制器会在分片超时后自动重试,而重试的分片和原本正在飞的分片可能同时在服务端落地。虽然最终内容一致,但如果写入逻辑不是幂等的,最后合并出来的文件就会损坏。

解决办法就是我上面代码里写的:后端接收分片时先检查目标文件是否已存在,且大小是否等于当前分片大小,满足条件就直接返回成功,不重复写入。这个幂等检查一定要放在写入之前,并且要用文件大小做二次确认,不能只看文件名存在与否。

另一个容易踩的坑是 Nginx 层面对请求大小和时长的限制。我遇到过用户传 5MB 分片时,Nginx 依然报 413,排查半天发现是有个旧的 server 配置没有覆盖到,还在用默认的 1MB 限制。后来我在 Nginx 配置里显式设了client_max_body_size 10m;和proxy_request_buffering off;,才彻底解决。

4.2 MD5 计算阻塞页面进程

前端用 spark-md5 计算大文件 MD5 时,页面会假死十几秒,用户体验非常差。我一开始没太在意,结果测试同学在低配 Windows 机器上测 2GB 文件,页面直接无响应,以为程序崩溃了,提了个严重 bug。

解决方法是引入 Web Worker 把 MD5 计算挪到后台线程。主线程只负责把文件句柄传给 Worker,Worker 内部拿到 ArrayBuffer 分片计算哈希,完成后把结果传回。这样用户在计算期间依然可以操作页面,体验好了不少。如果 Web Worker 的兼容性有顾虑(其实现在都很好了),还有一个兜底方案:计算期间显示进度条并禁止继续点击,至少让用户知道正在做什么,而不是面对一个卡死的页面。

4.3 合并后的文件与源文件不一致

这个问题是最危险的。有一次用户反馈某文件合并后打开提示损坏,我定位了很久,最后发现是前端切片时修改了chunkSize配置,而后端合并代码里的CHUNK_SIZE常量没同步更新。前端新切的分片大小是 2MB,后端按 5MB 算偏移量,所有偏移全部错位。

这个问题的根源在于前后端共享的分片大小没有一个统一校验机制。我后来在后端分片接收接口里增加了一个size参数,前端把每个分片的实际大小传过来,后端校验它和期望大小严格一致,不一致就拒绝接收并返回明确的错误信息。如果没有这个校验,错误分片会被悄悄写入磁盘,合并时才发现问题,排查成本极高。现在我把这个校验看作分片上传的必备措施,不是可选项。

另外,大文件在传输过程中可能因为硬件问题出现数据位翻转,即使分片大小、顺序都对,合并后的文件也可能有坏字节。为了防这种情况,我做的完整方案里,前端会提交整个文件的 MD5,后端合并完成后重新计算一次 MD5 并比对,不一致就返回错误,触发前端重新上传失败的分片。这一步虽然增加了计算开销,但能从根本上杜绝数据静默损坏。

4.4 超时、重试与分片清理策略

网络超时是分片上传最常见的问题。前端每个分片请求都要设置合理的超时时间,我一般设 60 秒,超过就自动重试当前分片,最多重试 3 次。重试要配合指数退避,第一次等 200ms,第二次 500ms,第三次 1s,防止大量重试请求同时涌向后端。如果重试 3 次仍然失败,就中止任务并定位到具体失败的分片序号,同时提供一个"失败重传"按钮,让用户只重新传失败的那个分片。

后端的分片清理也不能忽略。用户上传失败后可能永远不会再回来,临时分片目录里的垃圾数据会越积越多。我在项目里加了一个定时任务,每天凌晨扫描临时目录,删除超过 24 小时没有被修改的分片文件。这个策略基于一个假设:真正在传输过程中的分片一定会在 24 小时内被调用,超过 24 小时未动的分片基本可以认定为死数据。清理时要注意避开正在合并的文件,我给合并中的文件加了一个.merging标记,定时任务遇到带标记的目录就跳过。

4.5 常见问题速查表

问题现象可能原因排查与解决方案
前端报 413 错误Nginx 或网关限制了请求体大小检查所有代理层的client_max_body_size,统一调大
后端报 Multipart 解析失败Spring 配置的max-file-size小于分片大小在 application.yml 里调整spring.servlet.multipart.max-file-size和max-request-size
合并后文件损坏分片大小不一致导致偏移量错位前端分片接口传 size,后端严格校验;合并后比对 MD5
上传快完成时突然中断单个分片 TCP 连接超时前端做指数退避重试,后端保持分片幂等
断点续传查不到历史状态用户选择文件重新打开后浏览器重新生成了 MD5固定使用文件内容计算 MD5,不要用文件名加 size 拼标识
合并时提示分片缺失前端的并发调度器漏传了某个分片检查调度逻辑的边界条件,确保最后一个分片的end为file.size
内存暴涨前端把整个文件读入内存再切片始终用file.slice按需读取,不要把文件转成 ArrayBuffer 再切

5. 优化进阶:并发调度、秒传与性能调优

5.1 并发调度与带宽利用

基础版分片上传按顺序一个个传虽然能跑,但速度很浪费。浏览器和服务器之间的带宽是双向的,串行传输让整个链路的吞吐量只有实际带宽的一小部分。我用了一个简单的并发窗口模型:维护一个包含 4 个上传任务的活动集合,任意一个完成就立即从待上传队列里补充新任务,全程保持 4 个请求在飞行状态。

这里要特别注意一点,并发数是把双刃剑。并发太高,比如同时发起 20 个请求,反而会因为 TCP 拥塞控制和浏览器连接池限制导致整体速度下降。实测下来 3 到 4 个并发是最稳定高效的,如果你的网络环境好、带宽充足,可以适当调到 6 个,但别在 HTTP/1.1 下超过这个数字。如果是 HTTP/2 环境,连接复用能力强,并发可以适当放宽,但仍要控制单请求内存占用。

5.2 服务端性能与磁盘 IO 优化

分片上传是典型的 IO 密集型业务,后端代码大部分时间都在读写磁盘,CPU 反而很闲。生产环境部署时,我建议把临时分片目录放到独立的 SSD 或高性能磁盘上,并且和系统盘分开,避免分片写入和日志写入相互争抢 IO。

合并大文件时,不要用Files.copy直接合并所有分片为一个大流,那样实际上是在同一时刻把大量数据从磁盘读到内存再写回磁盘,峰值内存不好控制。我一直用固定缓冲区循环读写,8KB 或 16KB 的缓冲区是最稳妥的。虽然单次 IO 小一点,但整体稳定,不会因为文件大小波动而出现内存峰值异常。

另外一个容易被忽视的优化点是分片目录的 inode 压力。如果你每天有大量用户上传,每天产生的分片小文件数量非常可观,而默认文件系统的 inode 可能不够用。我在生产环境给临时目录挂载时,会统计每天分片文件数的预估峰值,提前把 inode 预留出来,同时严格保留定时清理任务,防止小文件无限堆积。

5.3 数据一致性保障

最后单独聊聊数据一致性。分片上传的分布式特征让一致性问题变得隐蔽:分片上传阶段成功不等于文件合并成功,文件合并成功也不等于文件内容正确。我在项目里建立了三重保障。

第一层是分片级校验。后端接收每个分片时校验大小,前端提交总分片数和文件大小,服务端在合并前验证所有分片的累计大小是否等于文件的file.size。

第二层是合并过程校验。合并阶段遍历分片时,任何一个分片缺失就立刻终止合并,并返回缺失序号,不生成半成品文件。这个强制中断策略很关键,因为半成品文件一旦被用户下载,问题就升级了。

第三层是最终结果校验。合并完成后计算整个文件的 MD5,和前端提交的 MD5 比对,不一致就触发告警并删除损坏文件。这一步可以拦截极少数硬件级别的数据错误。三层校验合在一起,我上线到现在没有收到过任何一起"文件上传后无法使用"的投诉。


最后再分享一条我个人的实操心得:做分片上传方案时,别急着写代码,先把"文件标识、分片编号、合并策略、校验策略"这四个决策定下来。这四个点直接决定了方案能不能支撑断点续传,也决定了线上出问题时你能不能在半小时内定位。我最初就是没想清楚文件标识的问题,导致断点续传时同名文件互相覆盖,后来花了两天重构。先把主流程想明白,再动手写代码,后面能省下大把修 bug 的时间。这套方案我后来在不同项目里重复使用过多次,核心逻辑基本不动,换的只是存储介质和并发策略,你按照上面的步骤落地,遇到具体问题再回头看这一篇的排查表,应该能少走很多弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询