先说个真实经历:前年给一个后台管理系统做微服务改造,单体应用里文件上传一直没啥问题,拆成SpringCloud之后,100M的压缩包直接传不上去了。一开始以为代码写错了,排查了一下午,最后才发现是网关超时加多实例Session不共享在捣鬼。这个事让我彻底意识到,百M级别大文件在SpringCloud架构里根本不是"上传接口怎么写"的问题,而是网关、服务实例、存储、前端一整条链路的事。这篇就把我在实际项目里折腾出来的方案完整拆开讲清楚,包括分片、校验、Redis状态记录、合并策略,以及各种常规文档里不会写的坑。
1. 百兆文件在微服务里难搞,难在网关和会话状态
1.1 网关超时:第一道隐形拦路虎
很多团队把服务拆成SpringCloud架构以后,第一个撞上的问题就是网关超时。单体应用时代,请求直接打到Tomcat,文件上传走的是Servlet容器那套,只要maxPostSize配得够大,前端把整个文件扔过来就行。但微服务架构里多了网关层,SpringCloud Gateway默认的response-timeout只有30秒左右,一个大文件在网络状况一般的情况下,传个几分钟很正常,网关直接给你一个504,前端一脸懵。
有人会说,那把网关超时调大不就行了?理论上可以,但问题在于网关层级的超时不是唯一的限制。服务注册中心(比如Nacos或者Eureka)、负载均衡策略、底层Tomcat的connection-timeout,还有中间可能穿插的Feign调用,每一层都有自己默认超时时间。你只调网关,不调下游服务,文件传到一半,下游服务自己先断了。调全部?融断器(Sentinel或Hystrix)又会跳出来,频繁触发熔断,整个服务链路直接瘫痪。所以第一个认知要扭转过来:百M文件传输不是调一个参数就能过去的,得从结构上规避"一次性传输整包文件"这个思路。
1.2 多实例下的状态不一致问题
SpringCloud架构下服务基本都会做多实例部署,两个副本后面挂一个负载均衡。传统上传方式如果依赖Session保存上传过程中的临时状态,问题就来了:客户端A把文件分片1发给实例1,分片2发给实例2,两个实例各存各的,临时状态不共享,合并的时候凑不齐。
有人觉得可以用Sticky Session(会话粘滞),让同一个用户请求始终打到一个实例上。可行,但运维成本高,而且SpringCloud生态里,负载均衡层随时可能因为服务上下线把会话打散。更优雅的做法是把"过程状态"从本地内存迁出去,存到Redis这类共享存储里。这样不管哪个实例收到分片,读到的都是一份完整的上传进度,天然支持多实例横向扩展。这个思路也是后面方案的核心前提。
1.3 SpringCloud技术栈带来的额外约束
除了网关和会话,SpringCloud生态还有几个对传输大文件不太友好的默认配置。比如Spring Cloud Gateway底层基于Netty,Netty的maxInitialLineLength、maxHeaderSize这些参数都是有默认上限的,文件分片如果走HTTP头传元信息,头一大直接报错。再比如前端走API网关上传文件时,如果用了multipart/form-data格式,网关默认的请求体大小限制也会拦截大请求。
这些限制表面上都是"配置文件加两行"的事,但如果你的目标是支持几百M甚至几G的文件,全链路放开缓冲区大小是非常危险的,网络慢的时候大量请求堆积在网关上,直接把网关内存打爆。所以大文件方案在设计之初就应该从"整包上传"切换到"分块传输",这是传统单体应用升级到SpringCloud之后必须要接受的一个现实。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 断点续传的本质:分片、指纹、状态记录与重组成一个闭环
2.1 断点续传到底断的是什么
从用户视角看,断点续传是"这次传了一半,下次继续传,不用重新开始"。但从技术视角看,我们要解决的是三个独立的问题:
- 网络连接中断怎么恢复?
- 服务端内存/磁盘里的临时数据怎么跨请求保留?
- 怎么确认恢复后拼接出来的文件是完整且正确的?
传统的整包上传法之所以难做断点续传,是因为它把文件内容、进度信息、元数据全部绑在一次请求里,请求一断,所有状态全部丢失。分片上传的本质是把这个"一次性请求"拆成多次幂等请求,每一次请求只负责文件中的一个片段,服务端通过对片段逐个确认来累计进度。这时候断点续传的"断"就不再是断整个请求,而是断某一个分片,恢复成本从"重传几百M"下降为"重传一个几M的分片"。
2.2 MD5指纹与秒传的基础逻辑
分片之后,文件本体被打散存储,但我们需要一个全局标识来告诉服务端"这个文件是谁"。最常用的方案是计算整个文件的MD5(也可以按分片算每个分片的MD5)。前端在正式上传前先读取文件,算出完整文件的MD5,把这个值作为上传任务的ID。这样做有额外两个好处:
- 服务端可以用MD5做去重判断,如果这个文件之前已经上传过且合并完成,直接返回"秒传"成功,不需要重新走一遍上传流程。
- 合并时可以对每个分片做MD5校验,哪个分片在传输过程中损坏了,就单独重传那个分片,不用全部作废。
需要提醒的是,百M级别的文件在浏览器里用JavaScript计算MD5,如果直接在主线程跑,页面会卡死。常规做法是放到Web Worker里异步算,或者使用SparkMD5这类库的增量计算能力,边读文件边算,避免一次性把文件读入内存。
2.3 分片大小的选择依据(百M级别)
分片大小没有统一标准,要根据文件典型大小、网络状况和服务端处理能力综合选。我给一个百M级别场景的参考:分片大小建议5M到10M之间。
- 如果文件是100M,按10M一个分片就是10片,重传成本可控;
- 分片太小(比如1M),分片数量暴增,每个分片都要走一次网络请求和Redis状态更新,网关压力大,MySQL或Redis的写放大也很明显;
- 分片太大(比如50M),一旦某个分片传输失败,重传代价接近重新上传,断点续传的意义就弱了。
实际项目里我会让前端快速探测网速,网速好就
