
做金融保险系统的Java开发最让人头疼的往往不是复杂的核保规则而是“附件上传”这种看起来不起眼、实际能磨死人的基础功能。理赔资料、保单扫描件、合同影印件动辄几十上百MB的Word、Excel一个案件文件夹拖进来几百个文件加起来几个GB也不稀奇。早期我做过一个理赔影像上传模块用户传一个60MB的Excel页面直接转圈两分钟中途网络一抖整个请求就废了用户骂完客服客服再找我最后我还要被业务方追着问“能不能优化一下”。后来我把上传逻辑改成切片算法搭配秒传判断和断点续传同样的大文件最快几秒就能进系统上传过程的稳定性也明显上了一个台阶。这篇就把我实际落地的方案、参数取舍和踩过的坑一次说清楚。1. 金融保险场景为什么必须做切片优化1.1 大附件不是“大文件”那么简单金融保险系统的上传场景有很强的行业特征和普通网站传头像、传图片完全是两回事。首先是文件格式高度集中在Word、Excel、PDF这类Office文档而且这些文件往往不是“生成后不再变”的静态文件可能是理赔调查员反复修订的版本每天都有新版本覆盖上传。其次是网络环境不可控总部内网用户还好外勤人员经常在客户现场、4S店、医院走廊用手机热点或4G/5G网络上传一个50MB的文件在这种网络下传失败太常见了。最后是系统对数据完整性的要求极高一个保险合同附件丢失后续核赔环节就可能出纠纷这种责任开发者担不起。传统上传方案是“一个文件一个HTTP请求”文件有多大就流式写多大前端把File对象直接post给后端。这种方案对10MB以内的小文档勉强能用一旦文件超过30MB问题立刻暴露服务器接收耗时变长中间任何一个网络抖动都导致前功尽弃Servlet容器默认的请求体大小限制也经常把大请求直接挡在门外更麻烦的是用户一旦传失败很少有人愿意重新选文件再传一次这种挫败感会直接影响业务办理效率。这时候切片算法的价值就非常明显了。它的核心思路是“化整为零”前端把大文件切成一串小分片按顺序逐个上传后端接收分片后暂存直到全部分片到齐再合并。一个60MB的文件拆成几百个1MB的切片每个切片都是一个独立的小请求单个请求的失败影响面被控制得很小网络抖动时只需要重传失败的切片而不是重传整个文件。1.2 先分清秒传和断点是两个能力很多工程同学把“秒传”和“断点续传”混为一谈设计时把它们绑在一起结果两头都没做好。秒传的本质是“不传”根据文件指纹判断服务器已有相同内容直接返回一个已存在的文件引用用户完全不用等上传过程。断点续传的本质是“接着传”已经上传成功的切片标记为完成后续只补传缺失的切片让上传过程可以从上次失败的地方继续。它们可以同时存在也可以单独存在但背后的实现前提完全不同。秒传依赖可靠的文件唯一标识。最常用的是文件内容的MD5或SHA-1散列前端选文件后异步计算后端拿到指纹后去文件索引表里查命中就直接关联到业务单上。这里有个很容易被忽略的点只靠文件名做秒传判断非常危险金融系统里同名文件内容不同太常见了理赔调查员很可能把“现场照片20241008.docx”改了一下内容又传一次文件名完全一样内容已经变了。后端必须校验完整内容指纹至少也得用“文件大小 内容散列”双重校验。断点续传则依赖切片状态管理。前端需要把每个切片的上传状态完整记录已成功的不再发送后端则需要根据切片索引判断连续性允许分片乱序到达最后在合并阶段统一校验。这里我必须强调不能只靠“客户端自报状态”来做续传服务端必须保存每个切片的完成状态。因为用户可能换了一台设备也可能清除了浏览器缓存如果服务端不认账前端本地状态再完整也没用。1.3 切片算法解决的三个具体问题把它落到这个场景里切片算法真正解决的是这么三件事第一请求粒度缩小超时失败的概率大幅下降。大请求容易被网关、中间件、容器掐断小请求基本不会。第二失败后的重试成本被限制在“切片”级别。网络抖动重传几个MB而不是重新传整个大文件用户体验完全不同。第三支持并发上传充分利用带宽。单个大文件的串行上传在弱网场景下往往跑不满带宽切片后再配合固定并发数上传吞吐量会明显提升。但切片算法也不是银弹。它会带来额外的计算和IO开销需要在后端做好临时目录、合并锁、状态记录等配套设计。我见过不少团队只做了前端切片后端还是把分片当成普通文件一股脑接收结果系统反而更慢。切片只是第一步关键在整体链路的设计。2. 切片参数设计先定规则再写代码2.1 切片大小不是越小越好切片大小是切片算法里最核心的参数也是很多新手最容易拍脑袋定的参数。选得太小比如64KB分片数量巨大每个分片的HTTP头部开销占比很高大量请求挤在网络上反而拖慢整体速度选得太大比如10MB分片数量太少单个分片失败后的重传成本又回到了“近似整文件传输”的老路。我在实际项目里常用的是200KB到2MB区间具体根据文件类型和网络带宽动态调整。普通文件上传我习惯用1MB一档。金融场景的Word/Excel附件大多是几十MB1MB切片大概控制在几十到一两百个请求浏览器并发上传时表现稳定。更精细的做法是动态切片文件小于20MB直接走整文件直传大于20MB再拆切片网络状况差的时候把切片调小网络好的时候调大避免“一刀切”带来的资源浪费。下面这张表是我在自己项目里用过的参考值对照着选基本不会出太大问题。文件大小范围建议切片大小并发数适用场景0 - 5MB不切片1日常小附件5 - 20MB1MB3中文件上传20 - 100MB1MB - 2MB3 - 5大Word/Excel/PDF100MB以上2MB - 4MB5 - 8超大案件材料包这里还要考虑后端接收端的承载能力。切片算法本质上把压力从“单次大请求”转成了“多次小请求”如果后端没有做任何并发控制或者临时目录的读写能力跟不上再合理的切片大小也会被IO瓶颈拖垮。我在后端采用信号量控制同一文件的最大并发切片数超过阈值的请求直接返回“稍后重试”避免蜂拥而至的切片把服务器临时目录打满。2.2 文件标识、切片命名和状态存储一次设计好切片命名不能随意命名规则直接决定断点续传能不能实现。我常用的设计是{fileId}_{chunkIndex}fileId由前端生成保证同一文件在多次尝试中保持一致chunkIndex从0开始递增。更严谨的做法是携带三段信息文件指纹前缀、全局唯一ID、切片的起止字节范围便于后端做校验。前端生成的fileId必须做到“一次生成、长期复用”这是断点续传的前提。如果每次刷新页面都重新生成fileId那之前传的切片就全部作废续传自然无从谈起。我见过一个项目把fileId用UUID随机生成刷新页面后UUID变了前一次上传到80%的进度全部丢失排查了很久才发现是这么个低级错误。生成fileId的规则最好基于文件内容稳定推导比如“文件名 文件大小 最后修改时间”做一个本地散列保证同一文件每次算出来的fileId一致。指纹计算方面MD5仍然是常见选择但在金融级别的系统里我建议用SHA-256降低碰撞风险。前端计算超大文件的完整散列本身也有性能问题我常用的策略是先用文件大小做快速排除再抽样计算只有真正需要判定秒传时才做完整散列。判断流程上先比较“大小 修改时间 文件头部若干字节”做初筛命中后再算完整指纹否则直接进入上传流程省去大文件全量计算的时间。状态存储也要设计清楚。前端可以用localStorage保存已上传切片索引数组但必须注意localStorage有容量限制切片特别多时可能存不下建议用BitMap压缩存储或者只保存“最后一个连续完成的索引中断点之后的重试列表”。服务端推荐用Redis保存切片状态每个文件一个Hash结构key是切片索引value是完成标记。这样查询哪些切片已完成只需要一次HKEYS操作非常高效。3. 核心流程实现秒传、断点、合并一条链路走通3.1 预检接口与秒传判定用户选择文件后前端先发起一个预检请求接口设计一般是POST/upload/precheck请求参数包含fileId、文件指纹、文件大小、文件名、业务单号。后端拿到指纹后查文件索引表如果命中相同文件记录就直接把已有文件ID返回前端结束上传流程业务系统把该文件ID关联到当前案件这就是秒传的完整链路。一个关键细节金融保险系统里的附件往往和具体业务单绑定同一个文件被多个案件引用时通常要做“引用计数”而不是复制文件实体。我的做法是文件存储层存一份实体文件关联表里每条业务记录指向同一个fileId这样既能实现秒传也能节省大量存储空间。删除附件时做引用计数判断只有最后一个引用释放后才能清理物理文件避免误删其他案件还在使用的材料。预检接口还有个重要职责返回后端已存在的切片列表。这既是秒传判断的补充也是断点续传的“进度同步”手段。前端拿到已存在切片列表后把本地待上传列表里对应的切片剔除只传缺失部分这相当于把断点续传和秒测在同一个接口里处理掉了减少一次HTTP往返。PostMapping(/precheck) public PrecheckResult precheck(RequestBody PrecheckRequest request) { // 1. 根据指纹查询文件索引表 FileIndex fileIndex fileIndexService.findBySha256(request.getSha256()); if (fileIndex ! null) { // 2. 秒传命中直接返回已存在文件 return PrecheckResult.seconed(fileIndex.getId()); } // 3. 查询该fileId已上传的切片列表 SetString uploadedChunks chunkStateService.getUploadedChunks(request.getFileId()); return PrecheckResult.continueUpload(uploadedChunks); }这段逻辑非常简单但有两个隐含点需要提醒一是颜色不要变预热、清理、缓存等操作都要随时考虑一致性二是预检接口本身要做时间戳和文件大小校验防止用户传了一个占用空间很小的同名文件指纹对不上却被误判为秒传。3.2 切片上传接口与并发控制切片上传接口的核心逻辑是接收一个分片并落盘更新状态。Spring Boot下我用MultipartFile接收切片直接transferTo写入临时目录。看起来简单但并发控制才是重点。我用的方案是每文件维度加信号量上限3到5个并发切片请求避免同一文件的所有切片同时涌进来打爆磁盘IO。PostMapping(/upload/chunk) public ResponseEntityChunkResult uploadChunk( RequestParam(fileId) String fileId, RequestParam(index) int index, RequestParam(total) int total, RequestPart(chunk) MultipartFile chunk) { // 1. 先校验切片大小是否在预期范围 if (chunk.getSize() MAX_CHUNK_SIZE) { return ResponseEntity.badRequest().build(); } // 2. 写入临时分片目录 Path chunkDir Paths.get(chunkStoragePath, fileId); Files.createDirectories(chunkDir); chunk.transferTo(chunkDir.resolve(index .part)); // 3. 更新Redis状态 chunkStateService.markChunkCompleted(fileId, index); return ResponseEntity.ok(new ChunkResult(index, true)); }接口本身不复杂但有几个细节决定成败。第一后端要校验total参数防止客户端把total传错导致合并时缺块第二MultipartFile的transferTo方法在部分容器实现里对大文件不太友好最好用InputStream逐段拷贝避免内存溢出第三写临时目录后最好落一次脏标记如果中间进程崩溃下次扫描能发现并清理半成品文件。我在生产环境里用文件重命名加状态表的方式写完.part后重命名为.done这样即使状态Redis丢了通过扫描目录也能恢复一部分已上传进度。3.3 合并接口与完整性校验所有切片上传完成后前端发一个合并请求后端检查切片完整性按顺序拼接生成最终文件。这里最容易出现“文件打不开”的问题原因往往是合并校验不到位。合并接口的流程我建议这样设计先加分布式锁保证同一个fileId只有一个合并任务在执行然后遍历所有切片索引检查是否每个.done文件都存在接着按索引顺序逐个写入输出流每写入一个切片都记录累计字节数最终与元数据中的原始大小比对最后做一次文件可读性校验Word和Excel文件可以用Apache POI打开尝试读取打不开就保留临时切片并返回错误让前端重新补传。PostMapping(/upload/merge) public ResponseEntityVoid merge(RequestParam String fileId, RequestParam int total, RequestParam long expectSize) { // 1. 加分布式锁防止重复合并 if (!lockService.tryLock(merge: fileId, Duration.ofSeconds(30))) { return ResponseEntity.status(HttpStatus.CONFLICT).build(); } try { Path chunkDir Paths.get(chunkStoragePath, fileId); Path target Paths.get(finalStoragePath, fileId . ext); long totalSize 0L; try (OutputStream out Files.newOutputStream(target)) { for (int i 0; i total; i) { Path part chunkDir.resolve(i .done); if (!Files.exists(part)) { throw new IllegalStateException(missing chunk: i); } totalSize Files.copy(part, out); } } // 2. 校验合并后的文件大小 if (totalSize ! expectSize) { Files.deleteIfExists(target); throw new IllegalStateException(size mismatch); } // 3. 清理临时目录与状态数据 chunkStateService.clearChunks(fileId); } finally { lockService.unlock(merge: fileId); } return ResponseEntity.ok().build(); }合并过程中的IO问题也很值得注意。如果直接把所有.done文件遍历一遍用Files.copy拼接几千个切片时的性能会很难看。我一般会加一层缓冲流并且把连续索引的切片先合并成几个大临时块再用多线程并行写最后统一校验。不过对于10MB级别的大附件单线程顺序拼其实够了先保证正确性再优化性能。3.4 断点续传的状态恢复策略断点续传的恢复策略本质就是“前端本地状态 服务端真实状态”做一次对比。前端刷新页面或者网络恢复后先调预检接口拿到服务端已完成的切片索引集合再与本地保存的已完成索引集合做差集差集部分重新上传。服务端状态是权威前端状态只是加速。我在Redis里存切片状态时用的是Hash结构key是chunk:fileIdfield是indexvalue是done。预检接口直接HKEYS获取全部已完成的field如果文件很大、切片数量很多这种方案在几百个切片级别是轻松的。如果切片数量过万就要考虑用BitMap存储Redis的SETBIT和后续遍历BITFIELD效率更高但代码复杂度会上升不少。还有一个细节容易被忽略服务端临时目录中的切片可能在合并完成后被清理但Redis状态可能残留。我的解决方案是在正式合并完成后统一删除Redis中对应fileId的所有状态键并且给所有上传入口加一个“目录不存在则自动重建”的容错逻辑避免状态残留和目录清理互相干扰。4. Word/Excel大附件与文件夹场景的特殊处理4.1 Office文档内部结构与合并校验Word和Excel看起来是单一文件但内部结构非常特殊。新版Office文档本质是ZIP压缩包里面包含多个XML和资源文件旧版.doc和.xls则是二进制格式。切片算法本身不关心内容结构上传过程只是字节流但金融系统经常需要把Office文档转PDF预览或者做内容抽取合并出来的文件如果字节顺序不对转码阶段就会直接报“文件损坏”。我实打实踩过这样一个坑某个Excel在用高并发切片上传时前端并发网路抖动导致其中一个切片被截断成半截合并接口只校验了文件大小发现大小对不上但没进一步处理直接把半截文件当成最终文件返回给用户了。用户下载后Excel打开就报错最后追查才发现是合并校验太粗糙。后来的方案是合并时逐个读取切片每读一个就记录实际字节数最终大小必须精确等于原始文件大小如果校验失败保留所有临时切片让前端只补传出错的那几个切片重新合并而不是让用户从头传。另外还有一种情况Office文档在上传过程中文件头部的ZIP签名PK和文件尾部的中央目录区都在切片里被拆散了如果合并时Index顺序错乱文件会损坏。所以合并阶段的顺序校验绝对不能省。4.2 文件夹批量上传的目录重建与过滤金融保险场景经常有“整个案件材料文件夹上传”的需求文件夹内部还有子目录。切片算法是针对单个文件的文件夹批量上传的本质是“遍历文件夹 逐文件切片上传”的组合。前端用Web API的directory模式递归遍历出文件清单保留相对路径后端用path字段作为附件归属路径合并后重建目录结构。这里有几个实际问题要处理。第一Office文档在Mac和Windows下会有额外元数据比如Mac的__MACOSX目录、.DS_Store文件Windows的Thumbs.db文件夹批量上传时这些隐藏文件很容易被当成附件传到系统里导致“一个文件夹传完文件数量和用户预期不一致”。我建议前端在遍历时默认过滤掉这些系统文件后端再兜底校验不允许非文档类扩展名混入。第二中文文件名和长路径问题。Windows下文件路径最长260个字符文件夹层级深了之后中文加长路径很容易触发“路径过长”。切片上传时前端上传的都是原始文件名后端存储层最好加一层内部ID映射展示层再恢复原始路径。这样做既能避免路径长度问题也能防止文件名带../等路径穿越字符引发安全问题。第三批量上传还要做好任务队列和总进度汇总。用户体验上文件夹上传不应该是一个文件一个文件的独立进度条而应该是一个总进度条。我的做法是先遍历文件夹内所有文件算出总字节数每个文件切片上传完成后按字节比例累加前端统一渲染总进度。后端并发切片的文件数控制在3到5个避免同一个文件夹内几十个文件同时打过来把服务器连接数打满。4.3 文件存储与临时空间治理切片上传的临时目录是很容易被忽略的“隐形磁盘杀手”。很多团队把切片落盘路径写在系统临时目录合并完之后忘记清理时间一长几十GB的垃圾切片就堆在那里。金融系统磁盘空间是稀缺资源我在生产环境专门写了一个定时清理任务每小时扫描临时目录找出超过24小时没有更新的fileId目录直接删除并同步清理Redis中的状态数据。另外最终文件的存储也要考虑去重和引用管理。同一个合同模板可能被几千个案件引用如果用“每案件一份独立拷贝”的设计磁盘浪费非常严重。我在文件索引表里维护了ref_count计数字段秒传命中时直接做引用加一不复制物理文件删除时引用减一减到零才真正删除文件。这样整个系统的存储占用能降低很多尤其在金融保险这种高重复度场景下效果非常明显。5. 常见问题排查与避坑实录5.1 秒传判断偶发失败同一文件第二次还要重新传这个问题绝大多数原因都在前端指纹计算上。排查时先看指纹是不是每次计算都一致尤其是抽样算法里如果采样位置依赖文件读取时的游标状态很容易出现前后两次采样结果不同。另外检查大小写MD5或SHA256统一转成小写再比对前端可能转了大写后端比对时不匹配导致秒传永远不命中。还有一种情况是文件确实没变但用户在上传前有临时修改了文件的“最后修改时间”如果指纹里包含了修改时间就一定会变化。我的建议是秒传判断的指纹尽量基于文件内容散列内容相同就是同一份文件和修改时间没有关系。5.2 断点续传时进度条总是从0开始先排查fileId是否每次刷新都重新生成。我前面说过fileId必须是一次生成、长期复用不能刷新就变号。确认前端已经把fileId按稳定规则存到localStoragekey建议由文件指纹加相对路径拼出来保证同一文件每次得到同一个key。其次后端预检接口要确认切片状态是持久化的而不是只放在内存Map里服务端一重启内存态就没了前端就算本地有状态后端不认账也只能全传。还有一种隐蔽场景用户换浏览器或换电脑后想续传本地状态彻底丢失服务端状态如果还存在预检接口返回已上传切片列表后也能续上这就是为什么我一直强调服务端必须是状态兜底本地状态只是加速措施。5.3 合并后的文件打不开第一步确认所有切片字节数之和是否等于文件原始大小第二步确认合并时是否按索引严格顺序拼接第三步确认合并完成后是否正确关闭输出流并flush。很多“文件打不开”其实是合并时多写了一段缓冲数据或者InputStream没有读完就关了流。推荐在合并完成后做一次“文件可读性探测”Word/Excel文件用Apache POI打开校验一次打开失败就保留临时切片并返回错误信息让前端补传缺失部分而不是让用户从头传一次。这段校验逻辑虽然会多花几十毫秒但能省掉大量用户投诉量。5.4 切片上传时服务器频繁报413或超时413错误通常是网关或容器对请求体大小做了限制切片请求本身很小按理说不应该触发但如果前端没有正确转二进制流而是把整个文件的Base64塞进JSON里那请求体大小限制一定会炸。确认前端用multipart/form-data分片传输而不是把切片数据转成JSON字符串。超时问题则要看后端接口的连接超时和读取超时配置小请求理论上传得很快但如果服务器负载高还是建议把读取超时放宽到30秒以上。5.5 常见问题速查表现象可能原因解决方法秒传不生效指纹不稳定或大小写不一致统一散列算法转小写比对进度条清零fileId每次重新生成按文件内容稳定生成fileId并持久化合并文件损坏合并时未校验完整性和顺序逐切片校验字节数校验大小总和服务器磁盘暴涨临时切片未清理定期清理超24小时未合并的目录并发上传变慢IO瓶颈或未做并发控制后端加信号量限流限制并发切片数文件夹文件变多Mac/Windows系统文件混入前端遍历时过滤隐藏文件后端扩展名校验6. 实测心得与后续优化这个模块上线之后我最直观的感受是用户根本不在乎你用了什么算法、拆了多少切片他们只关心三个问题——传上去了没有、快不快、传一半断了能不能接着传。把这三个问题解决得越彻底系统评价就越高。我在后续优化里还做了两个方向目前效果都不错。一个是超大附件场景下的“先压缩再切片”很多Word和Excel文档内容重复度高压缩后体积能缩小一半以上切片数量也相应减少上传时间直接省了很大一截。先压缩再切片有一个好处就是断点续传的重传量也随之减少但要注意压缩后再转PDF预览时需要先解压链路里多一层处理。另一个是秒传缓存的热度淘汰策略金融系统里高频复用的合同模板文件非常多我单独维护了一张热度表对频繁命中的文件缓存散列结果和存储ID减少每次预检都查主索引表的开销。最后分享一个我从这个项目带出来的通用经验做切片上传别一头扎进代码里先把“文件标识规则、切片大小、状态存储方案、合并校验策略”这四个约定在文档里写清楚前后端严格对齐后面能省掉大量联调和返工。我在第一次设计时就是后端已经写完接口前端还在纠结切片大小和fileId规则两个团队各做各的最后联调阶段多花了一倍时间才统一口径。把规则定在前面实现反而只是体力活。