FEATURED · 精选文章

Spring Boot异步编程实战:大文件上传并发瓶颈与分片合并方案

发布时间 / 2026/9/12 19:26:34
来源 / 创域科博编辑部
栏目 / 资讯中心
Spring Boot异步编程实战:大文件上传并发瓶颈与分片合并方案 1. 大文件上传同步阻塞的困境为什么必须“另起炉灶”先讲一个我实际遇到过的场景。某次给内部系统做一个资料库模块用户需要上传几百MB甚至几个GB的工程图纸和视频素材。第一版实现很“朴素”前端直接把文件POST到Spring Boot接口Controller里接收 MultipartFile然后循环字节流写入本地磁盘或者OSS。功能倒是跑通了结果一上生产就出事。用户传一个1GB的文件网络再快也要几十秒到几分钟这段时间里Tomcat的工作线程就一直占着不放。而有上传任务的同时其他用户查列表、点详情这些轻量请求也在同一个线程池里排队结果整个系统的响应时间从几十毫秒飙升到几十秒前端轮询接口全部超时服务端的监控图上线程池活跃数直接顶到上限。这其实是同步模型最典型的瓶颈——大文件上传是一个典型的IO密集型长耗时操作它消耗的不是CPU而是“线程占用时间”。Tomcat默认的线程池也就200个左右10个人同时传大文件线程池就满了应用基本处于半瘫痪状态。当时的第一反应是“要不要上消息队列”但仔细一想上传这个动作本身并不是一个可以简单丢进MQ就完事的操作文件数据还要写存储、还要记录元数据、还要通知下游处理。所以本质上要解决的是两件事第一让Web请求快速返回把耗时任务扔给后台去干第二让这个后台任务可控、可追踪、可重试。这正是Spring的Async体系擅长做的事情。当然这里要先把概念捋清楚。Spring里的Async异步开发指的是“方法的异步执行”——调用方发起调用后立刻返回真正的方法体逻辑由Spring管理的线程池去执行。它解决的是“任务执行时机”的问题而不是“文件传输通道”的问题。HTTP上传这个通道本身依然是同步的所以我们真正要设计的是一个组合方案利用Async把上传完成后的文件处理转存、合并、校验、入库从请求链路里剥离出去同时配合分片等手段减轻单次请求的压力。本文就以大文件上传这个场景为例完整走一遍Spring Async的实战落地过程。内容包括Async的生效机制和线程池配置、上传任务的状态追踪、分片合并的异步链路设计、异常处理与兜底方案以及我在生产环境里踩过的几个比较典型的坑。适合正在做文件上传类功能、想优化接口响应时间或者对Spring异步编程理解还停留在“加个Async注解”这个层面的同学参考。2. Async的生效机制与线程池调参这块才是重头戏2.1 为什么你的Async经常不生效先说一个老生常谈但杀伤力极大的问题Async注解默认情况下形同虚设的情况太多了。好多人写了个异步方法结果发现调用方还是同步等它执行完排查半天找不到原因。Async的底层是基于Spring AOP动态代理实现的。Spring容器在启动时会为标注了Async的Bean生成一个代理对象外部调用这个Bean的方法时实际上是调用代理对象的方法代理对象把方法提交给线程池然后立刻返回。这里有个关键前提——你的调用必须走代理。最常见的失效场景有三个同一个类内部的“自调用”。比如ServiceA的方法a()里直接调用了同类的方法b()而b()上标注了Async。这种调用绕过了代理对象直接在this引用上执行异步自然不生效。没有在配置类或启动类上加EnableAsync注解。没有这个开关Spring根本不会去解析Async注解。调用的Bean没有被Spring管理或者通过new关键字手动创建了对象。第三点尤其隐蔽有时候静态工具类里new了一个Service然后调它的异步方法显然是不会生效的。所以我的建议是异步方法一定要定义在独立的Bean里并且通过注入的方式调用同时启动类上确保有EnableAsync。如果想做得更严谨一点可以额外开启EnableAsync(proxyTargetClass true)强制走CGLIB代理避免因为接口代理导致的注解不被识别。2.2 默认线程池的隐患SimpleAsyncTaskExecutor真不能直接用如果你只加了EnableAsync和Async什么都没配置Spring Boot会使用默认的SimpleAsyncTaskExecutor。这个执行器的行为非常原始每次执行任务都new一个线程没有线程复用也没有最大并发数限制。高并发场景下线程数会无限制增长最终导致内存溢出或者频繁上下文切换系统直接卡死。这对于上传场景来说是不可接受的。文件上传后的处理任务往往比较重涉及IO读写、校验、入库并发量稍微一起来默认执行器会把应用拖垮。所以生产级用法必须自定义线程池并且明确指定给Async使用。Spring支持在配置类里定义一个名为taskExecutor的BeanAsync注解默认会优先查找这个名称的执行器。也可以通过Async(otherExecutor)指定使用某个特定名称的执行器。下面是我在实际项目里用的一个线程池配置参数经过压测调过大家可以参考Configuration public class AsyncConfig { Bean(fileProcessExecutor) public Executor fileProcessExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); // 核心线程数平时处理上传任务的常驻线程 executor.setCorePoolSize(8); // 最大线程数高峰期最多能扩展到多少线程 executor.setMaxPoolSize(16); // 队列容量缓冲的任务数 executor.setQueueCapacity(200); // 线程空闲存活时间超过这个时间没有任务则回收线程 executor.setKeepAliveSeconds(60); // 线程名前缀方便日志排查 executor.setThreadNamePrefix(file-process-); // 拒绝策略由调用者线程执行防止任务丢失 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }这里有几个参数值得单独说一下。corePoolSize设为8是考虑到文件处理任务主要是IO密集型的不是CPU密集型线程数不需要太多8个线程就能把磁盘和网络的IO带宽吃得比较满。maxPoolSize设16是给上传高峰留一定的弹性空间但也不能无限大因为每个线程在处理大文件拷贝时占用的内存和文件句柄都不小。队列容量200是控制缓冲任务的积压量如果200个都在排队说明系统处理能力已经到上限了这时候再往里塞任务意义不大。拒绝策略我特意选了CallerRunsPolicy。这个策略的含义是当线程池和队列都满的时候任务不会被丢弃而是由提交任务的线程也就是Web请求线程自己来执行。对于文件上传这个场景任务丢不得宁可让请求线程多等一会儿也不能把上传结果弄丢。这个策略在设计上会有个优点天然实现了“背压”——服务端处理不过来时请求方会感受到延迟从而自然降低提交速度。2.3 线程池参数设计的底层逻辑很多文章讲线程池就是堆一套计算公式什么CPU密集用N1、IO密集用2N。但文件处理这个场景更特殊它的耗时主体是磁盘IO和网络IO而且每个任务占用资源差异极大。传一个10MB的文件和传一个2GB的文件处理时长差了百倍。所以我在实际调参时不是照搬公式而是先看监控数据核心看三个指标线程池活跃线程数、队列积压量、任务平均耗时。以这些数据为基准调整参数。另外要特意提醒一个点除了文件处理的执行器我还专门定义了一个“任务状态查询执行器”和一个“临时文件清理执行器”。三者的职责严格分开。为什么因为不同的任务对线程池的需求不一样。文件处理任务耗时长、资源占用大适合配置独立的线程池状态查询任务要求响应快不能被长任务挤掉清理任务频率低、不能影响主线。如果全都混用一个默认线程池一个慢任务就可能拖垮所有异步逻辑。这一点类似数据库里“读写分离”的思路本质都是隔离不同负载之间的相互影响。3. 上传任务的状态追踪让异步链路不再“黑盒”3.1 任务ID与状态机设计异步化的第一个直接问题就是调用方立刻就返回了但用户怎么知道后台处理到哪一步了尤其是大文件上传用户最关心的就是“传完了没有、处理完了没有”。我这里的做法是设计一个“两步提交”的上传模型。前端先把文件对象的信息通过一个预请求告诉后端后端生成一个全局唯一的任务ID并初始化这条上传记录的状态然后把这个ID返回给前端。前端拿着这个ID去执行真正的文件上传。文件上传完成后后端在Controller里把这个任务ID标记为“已上传”同时触发一个Async的异步方法去做后续处理——比如转存到OSS、做MD5校验、生成缩略图、更新索引等。这里的核心是引入一个显式的任务状态机。我会定义一个枚举public enum UploadTaskStatus { INIT(0, 已创建), UPLOADING(1, 上传中), UPLOADED(2, 已上传待处理), PROCESSING(3, 处理中), SUCCESS(4, 处理成功), FAILED(5, 处理失败); }状态流转路径是INIT - UPLOADING - UPLOADED - PROCESSING - SUCCESS/FAILED。每一步都有动作触发。前端通过轮询或者WebSocket订阅任务ID的状态变化页面上的进度条就是跟着这个状态机在走。这一步看起来简单但它是异步链路能否在生产环境落地的关键——没有状态机就没有进度反馈没有进度反馈用户就会以为系统卡死了然后疯狂刷新页面造成重复上传。3.2 内存存储不可靠生产环境要用Redis刚开始做的时候我图省事把任务状态放在了一个ConcurrentHashMap里key是任务IDvalue是状态对象。单机演示完全没问题一旦部署到多节点问题立刻暴露用户第一次请求落在A节点状态写进了A节点的内存第二次查询被负载均衡转发到了B节点B节点压根不知道这个任务的存在。所以生产环境必须用外部存储。我选择用Redis原因很简单状态读写频繁需要极低的延迟任务状态本身不需要复杂的关系型查询Redis的TTL机制还能顺便清理过期任务。Redis里的数据结构我是这样设计的Key: upload:task:{taskId}类型是Hash存储任务的基本信息包括文件名、文件大小、分片总数、已上传分片数、状态、创建时间、更新时间等。Key: upload:task:{taskId}:progress类型是String存储一个百分比的数值用于前端展示进度条。Key: upload:task:{taskId}:shards类型是Set记录已经上传成功的分片编号。每次分片上传成功就通过Pipeline批量更新这几个Key避免频繁的网络往返。很多人会问为什么不直接更新数据库里的记录我的回答是数据库更新是最终一致性的保障但不是高频写入场景的首选。分片上传过程中每传一个分片就UPDATE一次数据库在高并发上传时会带来大量的行锁竞争和binlog写入压力。Redis做实时状态数据库做最终落库两者配合各司其职。3.3 数据库端的最终一致性落库异步任务处理完成后会把Redis里的状态同步回数据库。这里要注意数据库里的记录才是“权威数据”Redis里的状态可以允许短时间的延迟或丢失但数据库里不能丢。我用的表结构大致是CREATE TABLE file_upload_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id VARCHAR(64) NOT NULL, file_name VARCHAR(255) NOT NULL, file_size BIGINT NOT NULL, file_md5 VARCHAR(64), storage_path VARCHAR(512), status TINYINT NOT NULL, progress INT DEFAULT 0, error_msg VARCHAR(500), created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE KEY uk_task_id (task_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;taskId是业务侧的唯一标识数据库唯一索引能防止同一个上传任务被重复插入。状态更新时用乐观锁也就是UPDATE语句带status条件防止并发更新导致状态回退。比如UPDATE file_upload_task SET status 3, updated_at NOW() WHERE task_id #{taskId} AND status 2;这种写法保证了一个任务只能从“已上传”流转到“处理中”不会被两个线程同时推进到下一个状态。3.4 进度查询接口的优化思路前端轮询进度时如果每秒每个用户都来查一次Redis连接开销也不小。我的做法是在上传页面里把轮询间隔做成动态的上传期间每2秒查一次状态变成“处理中”后每5秒查一次超过2分钟没变化就直接提示用户联系管理员。另外查询接口要加一个简单的本地缓存比如Caffeine缓存1秒避免多个用户同时查同一个任务时打到Redis。这些细节看起来琐碎但大文件上传这种功能用户在一次上传流程里会和后端交互几十次甚至上百次任何一个环节的设计不当都会在大量用户同时使用时放大成性能问题。4. 分片上传 异步合并完整链路这样搭才靠谱4.1 分片是前提异步是手段真正面对GB级文件时单个HTTP请求把整个文件传完是不现实的。一方面是网络抖动会导致整个文件传失败另一方面是Web容器对请求体大小通常有限制。所以大文件上传必须做分片。前端把文件切成固定大小比如5MB一片的多个分片每个分片通过独立的请求上传。后端收到所有分片后再合并成完整的文件。分片上传本身和异步没有必然联系但“合并”这个动作往往耗时较长特别是当分片数量很多、文件总大小上GB时合并过程可能长达几十秒。如果合并放在请求线程里同步执行用户请求迟迟不返回体验很差还会占用Web容器线程。所以合并动作要交给Async线程池去处理。4.2 分片上传的临时目录与幂等设计每个上传任务在服务端分配一个独立的临时目录命名规则是taskId。目录结构如下/tmp/upload/{taskId}/ ├── 0.part ├── 1.part ├── 2.part └── ...前端每次上传分片时请求参数带上taskId、shardIndex、totalShards、fileMd5。后端根据taskId找到对应目录把分片内容写入shardIndex.part文件。由于HTTP请求可能重试同一个分片可能被提交多次所以写入时要做幂等处理如果对应分片文件已存在且大小匹配直接返回成功不重复写入。这里有个性能细节分片写入不推荐用MultipartFile.transferTo()直接落到临时文件而是先用FileOutputStream写入然后立即调用flush和fsync吗其实不一定fsync是一个重操作每个分片都fsync会拖慢速度。我的做法是在合并阶段统一做完整性校验通过MD5分片落盘阶段只保证数据安全落到了操作系统的Page Cache不强制fsync。如果进程崩溃本次上传任务直接标记失败客户端重新上传即可不需要做到分片级别的崩溃恢复——因为大文件上传的常态就是失败重来代价相对可控。分片全部传完后前端调用一个“通知合并”的接口。这个接口很重要它标记当前任务的分片已齐全触发合并任务。合并接口要做两个校验一是检查已上传分片数量是否等于totalShards二是校验每个分片文件的MD5是否等于前端上报的对应MD5。都通过后才把合并任务丢给异步线程池。4.3 异步合并的核心逻辑合并方法定义在独立的FileMergeService中用Async(fileProcessExecutor)标注。核心逻辑首先是创建一个足够大的输出文件然后按照分片序号从小到大循环读取分片文件写入输出流。这里要注意写入缓冲的大小我一般设置为8MB的byte[]避免频繁的小块IO。合并完成后计算整个文件的MD5与前端上报的文件MD5做比对。不一致说明数据在传输或落盘过程中出了问题任务标记FAILED通知前端重新上传。一致则继续执行后续业务比如把文件从临时目录转移到正式存储目录或者上传到OSS。整体代码结构大致如下Service public class FileMergeService { Autowired private UploadTaskRepository taskRepository; Async(fileProcessExecutor) public void mergeAsync(String taskId) { UploadTask task taskRepository.findByTaskId(taskId); if (task null || task.getStatus() ! UploadTaskStatus.UPLOADED) { return; } taskRepository.updateStatus(taskId, UploadTaskStatus.PROCESSING); try { // 1. 合并分片 File mergedFile mergeShards(task); // 2. MD5校验 String md5 FileUtils.calcMd5(mergedFile); if (!md5.equalsIgnoreCase(task.getFileMd5())) { throw new BusinessException(文件MD5校验失败); } // 3. 转存正式目录 String storagePath moveToStorage(mergedFile, task); // 4. 更新记录 taskRepository.updateSuccess(taskId, storagePath); } catch (Exception e) { log.error(merge task failed, taskId{}, taskId, e); taskRepository.updateFailed(taskId, e.getMessage()); } } }这里有个非常重要的教训Async方法的方法体内一定要自己try-catch所有异常。因为异步方法的异常不会像同步调用那样直接抛给调用方如果不在方法内部捕获异常就会丢失任务状态永远卡在“处理中”而且日志里什么都看不到。后面专门有一节讲异常处理这里先记住这个原则。4.4 合并失败的重试与补偿机制合并是一个相对脆弱的操作磁盘满了、文件被清理了、权限不对都可能导致失败。我设计了最多3次自动重试每次重试间隔10秒、30秒、60秒呈指数退避。重试机制的实现不是用简单的for循环而是借助Spring的Retryable注解配合Recover方法或者自己用定时任务扫描“处理中”状态超过5分钟的任务重新触发合并。我推荐后者因为它在应用重启后依然能执行定时任务启动时会扫描一遍脏数据而Retryable在应用重启后就会丢失重试状态。定时任务这里有个细节需要注意补偿任务一定不能和主要异步任务共用同一个线程池否则会因为排队拥挤导致补偿不及时。我单独配置了一个核心线程数为2的定时任务线程池专门做扫描和补偿。4.5 临时文件的清理策略分片合并完成后临时目录里的分片文件就没用了。如果不能及时清理磁盘很快就会满尤其是大文件场景一个任务1GB100个任务就是100GB。这个坑我实打实踩过当时生产磁盘告警排查后发现全是/tmp/upload下的残留分片。我现在的做法是合并成功后立刻清理当前任务临时目录对于上传失败、超时未完成的任务由一个定时任务每天凌晨扫描临时目录删除文件修改时间超过24小时并且任务状态不是“上传中/处理中”的目录。注意不能粗暴地按时间删必须结合任务状态判断避免误删正在上传的文件。5. 异常处理与兜底方案异步方法出了问题别让用户背锅5.1 异步任务是“静默失败”的重灾区前面提到Async方法抛出的异常不会传给调用方。默认情况下Spring会把这个异常交给SimpleAsyncUncaughtExceptionHandler处理它只会打一条WARN日志然后什么都没有。如果业务代码里没有内部try-catch这个任务就无声无息地失败了用户看到的是任务状态永远转圈开发者看到的是日志里一行容易忽略的警告。所以生产级异步开发第一件事就是自定义AsyncUncaughtExceptionHandler把所有异步方法的未捕获异常统一收拢起来写业务日志、推告警、标记任务失败。5.2 统一异常处理器的实现Spring提供了AsyncConfigurer接口我们可以实现它来指定全局的异步异常处理器。Configuration EnableAsync public class AsyncConfig implements AsyncConfigurer { Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(8); executor.setMaxPoolSize(16); executor.setQueueCapacity(200); executor.setThreadNamePrefix(global-async-); executor.initialize(); return executor; } Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (throwable, method, params) - { // 这里务必要做两件事 // 1. 打印完整堆栈到错误日志 log.error(异步方法执行异常, method{}, 参数{}, method.getName(), Arrays.toString(params), throwable); // 2. 发送告警通知钉钉/邮件/监控平台 alertService.sendAlert(异步任务异常, method.getName(), throwable); }; } }注意这里返回的Executor会成为Async的默认执行器所以前面提到的那种针对文件处理场景的特化线程池如果需要在特定方法上使用就通过Async(fileProcessExecutor)显式指定。5.3 使用CompletableFuture承接异步结果如果你的异步方法需要向调用方返回结果不要用void也不要直接返回Future推荐使用CompletableFuture。它的优势是组合能力极强可以方便地处理回调、异常、超时。Async(fileProcessExecutor) public CompletableFutureUploadResult processFileAsync(String taskId) { try { UploadResult result doProcess(taskId); return CompletableFuture.completedFuture(result); } catch (Exception e) { return CompletableFuture.failedFuture(e); } }调用方通过whenComplete、exceptionally等回调方法拿到结果或异常比较接近同步代码的体验。在超时控制方面Future.get(timeout, TimeUnit.SECONDS)是最后一道兜底防线防止因为下游系统卡死导致任务无限等待。5.4 应用关闭时的优雅停机异步任务最大的隐性问题就是线程池里还有任务在跑应用却被强制关闭导致任务丢失。Spring Boot生命周期里的PreDestroy方法可以帮我们做优雅停机。Bean(fileProcessExecutor) public ThreadPoolTaskExecutor fileProcessExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); // ... 参数配置 ... executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor; }setWaitForTasksToCompleteOnShutdown(true)会保证应用关闭时线程池不再接收新任务并等待已提交任务执行完毕setAwaitTerminationSeconds(60)设置最长等待60秒超过则强制关闭。对于文件合并这种重任务60秒通常够用如果任务特别重可以适当调大到120秒。5.5 时间轮与兜底定时任务上面提到过定时扫描做重试我再补充一个细节。定时任务扫描的SQL建议写成这样SELECT task_id, file_path FROM file_upload_task WHERE status 3 AND updated_at DATE_SUB(NOW(), INTERVAL 5 MINUTE) LIMIT 50;status3是“处理中”如果5分钟还没结束大概率是异常了。扫描出来后先把任务状态重置为“已上传”状态再重新提交异步合并。注意要限制扫描数量避免一次性捞太多把线程池塞满。6. 从这次上传改造中沉淀下来的几个实战判断6.1 异步不是银弹先想清楚瓶颈在哪这次改造完成之后我复盘了一段时间最大的体会是异步化解决的是“请求线程被长任务阻塞”的问题但它没有减少任务本身的工作量。大文件上传的瓶颈往往在磁盘IO和网络带宽不是线程数。如果你不做分片、不做断点续传单纯把合并逻辑丢给异步那么效果只是“用户请求返回快了”但服务端实际处理时间并没有缩短甚至因为线程池排队还可能变慢。所以异步开发的前提是你已经找到了真正的瓶颈并且异步能让系统整体吞吐量提高。大文件上传场景里异步化的核心收益是释放Web容器线程让轻量请求不被长任务拖死同时合并、校验这类动作可以和下一个文件的上传并行推进。6.2 线程池参数要按“隔离监控压测”的套路去调生产环境里我见过太多人把线程池参数写成“网上抄的默认值”然后就不管了。这种做法在文件上传场景很容易出事因为不同项目的文件大小、并发量、存储介质差异太大了。我建议的套路是三步第一步把执行器独立配置线程名前缀写好方便日志检索第二步上线后盯一周监控重点看线程池活跃度、队列堆积时长、任务完成耗时第三步根据监控数据做压测找到线程数和吞吐量的拐点再回调参数。不要迷信网上的推荐值每个系统的负载模型都不一样。6.3 状态机是异步链路的“眼”没有状态机的异步任务就是个黑盒出了问题你根本不知道任务卡在哪一步。文件上传场景尤其如此因为它的链路特别长上传分片、合并文件、校验MD5、转存、入库、通知回调每一步都可能失败。状态机配合日志能让你在用户投诉之前就发现问题。另外状态机的设计要控制流转方向避免出现“处理中”被并发请求改回“已上传”这种状态回退。数据库更新时带上“当前状态”的条件就是最简单有效的乐观锁。6.4 关于“前端使用Worker上传”和Spring结合的一点思考热搜词里有一条“前端使用Worker上传大文件”这个方向我最近也在研究。前端用Web Worker在后台线程里做分片、算MD5可以避免主线程卡顿提升用户体验。但要注意前端的Worker只解决浏览器侧的UI阻塞问题服务端的异步设计是一个独立问题。两者不冲突但也不能互相替代。从服务端的角度看无论前端怎么切分最终到达后端的就是若干个分片请求。后端的核心设计目标始终是快速接收分片、安全存储、正确合并、及时反馈状态。前端怎么优化UI后端不用管太多但接口的幂等性、状态机的健壮性后端必须把好关。6.5 高频小细节日志、监控、告警不能省最后聊聊一些容易被忽视的细节。异步方法里的日志要打上taskId和线程名方便追踪。日志级别要用INFO记录任务开始和结束用ERROR记录异常不要把所有日志都打在INFO级别否则出了问题排查成本很高。监控方面ThreadPoolTaskExecutor可以暴露到Spring Boot Actuator的metrics里线程池活跃数、队列大小、任务完成数都能看到。我每次发版后都会先看一眼这个指标确认异步任务没有异常堆积。告警规则可以设为队列积压超过阈值持续5分钟或者任务失败率超过1%就触发告警推送。这些细节单独看都不起眼但它们组合起来决定了你做的异步功能是“能用的demo”还是“扛得住线上流量的功能”。我从第一次上线时对着日志茫然无措到现在能通过监控面板快速定位问题靠的就是把这些细节一点点补齐。如果你现在正准备改造大文件上传我建议不要一上来就追求架构大而全。先把分片上传做扎实再把合并逻辑异步化配上状态机和异常处理就已经能解决80%的问题了。剩下的20%等真正遇到了性能瓶颈或者新的业务场景再逐步演进也不迟。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻