资讯动态

Java实现银行超大文件分块上传:从架构设计到落地实践

发布时间:2026/10/2 8:49:57 来源:尧图企业网站定制
做银行项目的时候我遇到过一个特别头疼的需求业务方要在一个网页应用里上传几个GB大小的对账文件和影像资料包。最初用普通multipart方式直接传结果文件一过2GB要么页面转圈转到超时要么中间某层设备直接掐断连接。后来把方案彻底改成JAVA分块上传整套流程才算跑通。这篇就把我在银行网页应用里实现超大G级文件分块上传的完整思路拆开讲包括为什么非分块不可、后端接口怎么设计、合并怎么落盘、校验怎么保证数据一致性以及那些不试一次根本发现不了的坑。1. 银行场景下的G级文件上传到底卡在哪里1.1 文件大只是一方面更麻烦的是链路长很多人觉得文件上传嘛无非就是文件大小的问题。在普通互联网应用里偶尔传一个几百MB的视频用整文件直传勉强也能跑但在银行场景里问题要复杂得多。我接触到的银行文件上传需求主要包括这几种渠道系统的批量交易流水文件一个文件经常几个GB信贷审批件的影像资料包按客户打包之后体积很惊人对账系统每天的清算文件月末、季末峰值能到10GB以上监管报送类数据文件动辄就是超大附件这些文件还都不是在干净的实验室网络里传。银行的生产网络经过多层安全设备请求要过防火墙、WAF、负载均衡、文件过滤网关每一层设备对请求体大小、连接时长都有各自的限制。你辛辛苦苦写好一个上传接口本地测得好好的一上生产就发现中间某台设备默默给你断掉了。而且银行内部网络环境差异非常大。总行机房里的专线带宽充沛但一线分行网点可能只是普通办公网络上传一个5GB的文件耗时要按小时计算。如果中途断掉整文件直传就得从头再来这种体验放在银行这种对账周期卡得死死的场景里业务人员会疯掉的。所以超大G级文件这四个字关键不在文件大而在从浏览器到服务器这条链路上有太多环节对单次请求的体积和时长不友好。分块上传的本质就是把一个巨大到脆弱的请求拆成一小份一小份足够健壮的请求。1.2 传统上传方式在G级文件面前的问题我总结了一下用传统multipart整文件上传在G级文件这个量级上会遇到这么几类问题问题表现后果请求体过大超过负载均衡或Web容器限制直接收到413或连接被重置超时传输时间超过中间设备阈值服务端还在跑客户端已超时内存压力服务端接收时缓冲整个文件JVM堆内存溢出或频繁GC无法续传网络抖动导致传输中断从头再来时间成本翻倍无法校验传输完成后文件是否完整不确定数据出错对账不平这里有个很多人忽略的点浏览器层面虽然对上传没有硬性超时但服务器前面的那一堆中间设备是有超时阈值的。Nginx默认的proxy_read_timeout就是60秒很多银行自研的安全网关也设置了类似的限制。一个G级文件即便带宽尚可分钟级以上的传输时间很容易踩中这些上限。内存压力的问题更隐蔽。你用Spring MVC接收multipart文件默认情况下Tomcat会把请求体解析到临时目录但如果代码写法不当比如一次性file.getBytes()整个文件就被加载进堆内存了。我在测试环境做过一次演示一个2.4GB的文件直接读进内存GC立刻变得异常频繁服务响应时间飙升到几十秒。这在生产环境是不可接受的。2. 分块上传的流程设计与底层原理2.1 一次完整的分块上传会话是如何运作的分块上传的核心思路是前端把一个大文件切成若干固定大小的分片逐个上传到服务端全部上传完成后服务端再将分片按顺序合并成完整的文件。一个标准的流程是这样前端切分文件计算文件总大小、总块数创建上传会话服务端返回一个全局唯一的uploadId前端逐块上传分片数据服务端校验每块的完整性所有分片上传完成后前端触发合并请求服务端合并分片为完整文件然后释放临时资源这个过程我在JAVA后端里设计成了四个核心接口POST /upload/session创建上传会话接收文件名、文件大小、分片大小返回uploadIdPOST /upload/chunk上传单个分片携带uploadId、分片序号、分片数据POST /upload/complete通知服务端所有分片已上传触发合并GET /upload/status/{uploadId}查询会话状态用于断点续传和进度展示这里面有个细节值得多说一句为什么创建会话时要传文件大小和分片大小因为服务端要根据这些参数预校验磁盘空间、判断分片数量是否合理并且在合并前可以提前知道期望的文件总大小给最终校验提供依据。2.2 为什么选固定分片而不是流式直传我见过不少设计方案上来就搞流式直传、WebSocket推流听起来很高级但放在银行场景里我强烈不建议。原因很简单越简单的东西越可靠。固定分片方案的优势是第一天然支持断点续传。前端每传完一个分片服务端就标记这个分片已完成。网络中断后重新上传只需要查询一下哪些分片已完成跳过去接着传剩下的就行。这个特性对弱网环境下的G级文件是刚需。第二单次请求体积可控。我用过64MB和128MB两种分片大小。控制分片大小就能确保单个请求不会触发中间设备的各种限制。这比指望中间设备为你开绿灯现实得多。第三并发能力可控。分片之间是独立的理论上可以并行上传。在浏览器里用XMLHttpRequest并发3-5个分片比单连接顺序上传快得多又不会把带宽完全占满影响其他业务。第四失败成本极低。一个分片传失败了重新传的只是这个分片而不是整个文件。在网络不稳定的生产环境里这个优势是致命的。2.3 分片大小怎么定数学账算清楚分片大小的选择我直接讲结论银行内网环境建议64MB外网环境建议建议8-16MB。但这个结论是怎么来的我得解释一下否则你换一个场景就不知道怎么调了。分片大小主要受两个因素制约分片太小分片数量 文件大小 / 分片大小分片越多请求数越多每个请求都有HTTP头开销、数据库状态写入开销总开销会变得很大。一个4GB文件如果分片定成1MB会产生4096个请求光请求头开销和状态记录就是个灾难。分片太大单请求传输时间过长容易触达中间设备超时阈值。如果网络带宽只有5MB/s一个64MB分片需要12秒这还在安全范围内但如果带宽只有1MB/s64MB分片需要超过1分钟就可能触发Nginx或网关超时。所以分片大小的确定本质上是在请求数量和单请求耗时之间取平衡。你可以用这个公式粗略估算分片大小 期望单分片耗时(秒) × 最低保障带宽(MB/s)。假设你期望单分片20秒内传完最低保障带宽2MB/s那么分片大小取40MB以内比较安全。场景不同参数就要跟着调不能一个配置走天下。我把分片大小放到了配置中心前端通过一个配置接口动态获取这样以后调整不用重新发版。3. JAVA后端的核心实现方案与代码骨架3.1 会话设计一张表管理所有状态分块上传和普通上传最大的架构差异在于引入了会话这个概念。普通上传是一次性动作而分块上传是一个持久化的过程可能跨越几分钟甚至几小时所以必须有状态管理。我在数据库里设计了这样几张表upload_session上传会话表字段类型说明idvarchar(64)主键即uploadIdfile_namevarchar(255)原始文件名file_sizebigint文件总大小字节chunk_sizeint分片大小字节total_chunksint分片总数uploaded_chunksint已上传分片数statusvarchar(20)会话状态INIT/UPLOADING/MERGING/DONE/FAILEDcreate_timedatetime创建时间update_timedatetime更新时间upload_chunk分片记录表字段类型说明upload_idvarchar(64)所属会话IDchunk_indexint分片序号chunk_hashvarchar(64)分片内容的MD5sizebigint分片实际大小statusvarchar(20)分片状态UPLOADED/MERGED为什么状态要落库而不是放在内存里因为银行应用通常是集群部署前面挂了负载均衡同一个上传会话的请求可能打到不同的JVM实例。状态放在JVM内存里第二次分片请求被转发到另一台机器就查不到了。用数据库存状态天然支持多实例共享。Redis做状态存储也可以但考虑到分片数据本身落在文件系统会话状态和磁盘文件的对应关系需要持久化我还是选择了数据库为主、Redis缓存为辅的方式。3.2 上传分片的处理逻辑先落盘后校验以Spring Boot为例我在Controller里接收分片数据核心逻辑可以这样组织RestController RequestMapping(/upload) public class ChunkUploadController { PostMapping(/chunk) public Result uploadChunk(RequestParam(uploadId) String uploadId, RequestParam(chunkIndex) int chunkIndex, RequestPart(file) MultipartFile file) { // 1. 根据uploadId查询会话信息 UploadSession session uploadSessionMapper.selectById(uploadId); if (session null) { return Result.error(上传会话不存在); } // 2. 校验分片序号合法性 if (chunkIndex 0 || chunkIndex session.getTotalChunks()) { return Result.error(分片序号越界); } // 3. 校验分片大小最后一片允许小于分片大小 long expectSize (chunkIndex session.getTotalChunks() - 1) ? session.getFileSize() - (long) session.getChunkSize() * (session.getTotalChunks() - 1) : session.getChunkSize(); if (file.getSize() ! expectSize) { return Result.error(分片大小不匹配期望 expectSize 实际 file.getSize()); } // 4. 计算分片MD5 String md5 DigestUtils.md5DigestAsHex(file.getInputStream()); // 5. 分片数据落盘 Path chunkPath Paths.get(storageRoot, uploadId, String.valueOf(chunkIndex)); Files.createDirectories(chunkPath.getParent()); try (InputStream in file.getInputStream()) { Files.copy(in, chunkPath, StandardCopyOption.REPLACE_EXISTING); } // 6. 记录分片状态幂等存在则更新不报错 UploadChunk chunk new UploadChunk(); chunk.setUploadId(uploadId); chunk.setChunkIndex(chunkIndex); chunk.setChunkHash(md5); chunk.setSize(file.getSize()); chunk.setStatus(UPLOADED); uploadChunkMapper.upsert(chunk); // 7. 更新会话已上传数量 uploadSessionMapper.incrementUploadedChunks(uploadId); return Result.ok(分片上传成功); } }这段代码有几个关键点值得细讲先落盘再写状态。分片数据写入文件和更新数据库之间是有时间差的如果先写数据库再落盘失败了会出现数据库显示已上传但实际文件缺失的脏状态。我选择先落盘、后写库两个步骤之间即使程序崩溃最多是文件存在但状态未更新不会出现相反的情况。幂等处理很关键。前端在弱网环境下会重试分片同一个分片可能上传两次。upsert操作保证第二次上传不会报错而是直接覆盖旧分片。这个设计在做断点续传时尤其重要。3.3 合并策略顺序合并与FileChannel零拷贝所有分片上传完毕后前端调用complete接口服务端开始合并。合并的核心代码如下public void mergeChunks(String uploadId) throws IOException { UploadSession session uploadSessionMapper.selectById(uploadId); // 前置校验分片数量必须齐 int uploaded uploadSessionMapper.countUploadedChunks(uploadId); if (uploaded ! session.getTotalChunks()) { throw new IllegalStateException(分片未齐已上传 uploaded / session.getTotalChunks()); } Path uploadDir Paths.get(storageRoot, uploadId); Path mergedFile Paths.get(storageRoot, uploadId _merged); // 按分片序号顺序合并 try (FileChannel out FileChannel.open(mergedFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { for (int i 0; i session.getTotalChunks(); i) { Path chunkPath uploadDir.resolve(String.valueOf(i)); try (FileChannel in FileChannel.open(chunkPath, StandardOpenOption.READ)) { long position out.size(); long remaining in.size(); while (remaining 0) { long transferred out.transferFrom(in, position, remaining); position transferred; remaining - transferred; } } } } // 合并后清理分片目录 // ... 删除uploadDir下的所有分片文件 }这里用到了FileChannel.transferFrom走的是操作系统级的零拷贝机制数据从磁盘到磁盘的拷贝不经过JVM堆内存对大文件合并的性能提升非常明显。我做过对比用传统InputStream逐字节读写的合并2GB文件大概要40-50秒用FileChannel零拷贝同样是机械硬盘基本10秒以内搞定。在银行老旧的存储设备上这个差距是决定性的。合并过程要不要加事务这里有个容易搞错的点。MySQL事务管的是数据库管不了文件系统。合并过程涉及大量IO操作如果包在数据库事务里长事务会锁表、消耗连接反而危险。我的做法是合并前把会话状态置为MERGING合并完成后把状态置为DONE文件路径写入记录如果合并过程抛异常状态保持MERGING由定时任务扫描重新触发合并。这个状态机设计比数据库事务可靠得多。3.4 存储选型银行常用的几种方案对比分片和最终文件的落盘选什么样的存储我在这块也踩过不少坑。银行生产环境通常不会用简单的本地磁盘目录常见的方案有方案适用场景注意事项共享存储/NAS银行传统架构常见多实例共享注意NAS本身IO能力G级文件并发合并时要监控吞吐对象存储私有云方案如S3兼容存储分片可以直传对象存储合并用服务端拷贝HDFS大数据平台集成适合批量导入不适合实时分片合并本地磁盘同步最简单适合非核心系统要配置定时清理防止磁盘打满我的经验是如果只是做一个文件上传服务优先考虑对象存储或者NAS。本地磁盘看上去简单但生产上你会遇到磁盘空间管理、多实例文件不一致等问题够你喝一壶的。4. 文件校验与数据一致性银行绝不接受差不多4.1 三重校验分片级、文件级、业务级银行的数据敏感性决定了文件上传绝对不能出现传完了但文件是坏的这种情况。我在传输链路上设计了三个层次的校验。第一层分片MD5校验。每个分片上传时前端计算分片的MD5放到请求头或表单字段里后端收到分片后重新计算MD5并比对不一致直接拒绝。这一步保证的是传输过程中单个分片没有损坏。第二层合并后全文件校验。全部完成合并后如果前端在上传会话创建时提供了整个文件的MD5服务端在合并完成后重新计算整个文件的MD5并比对。这一步保证的是所有分片拼接在一起后和原始文件完全一致。第三层业务校验。文件合并完成后不是直接认为万事大吉。我建议加一道解析器校验比如压缩包就尝试解压读取目录结构、CSV就检查表头和行列数、PDF就检查页数。这一层校验一般业务系统按需实现但对银行场景尤其重要因为文件最终是要进业务系统的格式不对比内容不对更难排查。4.2 合并后全文件校验的成本控制这里有个实际问题一个5GB的文件合并后要计算整个文件的MD5这个操作本身就要读一遍全文件耗时不可忽略。怎么优化我的做法是分阶段校验合并过程本身是顺序读分片、顺序写合并文件我在合并的同时对每个分片已经做了一次完整性校验分片大小和MD5在落盘时已验证过合并完成后真正的全文件MD5计算放在一个异步任务里执行不阻塞complete接口的响应complete接口先返回合并完成校验中的状态前端轮询status接口获取最终结果这样用户体验是合并基本秒级完成校验在后台跑最终状态查询时拿到校验通过/校验失败。校验失败就自动清理合并文件触发补偿流程重新合并。4.3 并发上传时的数据一致性分片上传天然支持高并发但并发带来的一致性问题也需要处理。我遇到过的最典型问题是同一个uploadId的多个分片并行上传时数据库的uploaded_chunks计数可能不准确吗答案是如果简单用SELECT COUNT(*)统计没问题但如果用一个字段uploaded_chunks做累加并发更新下会有丢失更新的问题。我用的是UPDATE upload_session SET uploaded_chunks uploaded_chunks 1 WHERE id ?这个原子自增语句数据库层面解决并发计数问题不会出现少计或多计。合并时并发问题。合并操作必须保证只有一次。我用了数据库乐观锁UPDATE upload_session SET status MERGING WHERE id ? AND status UPLOADING如果更新影响行数为0说明已经有其他线程在合并了当前请求直接返回合并中。4.4 防重传与重复提交接口层面的幂等设计分片上传接口必须设计成幂等的这是银行系统上线评审时一定会被问到的问题。分片的幂等方案已经说了先查重已存在的分片直接覆盖或跳过。完整会话层的幂等多了一层前端可能因为超时重试同一个complete请求发两次。第二次进来时分片已经合并完了文件已经生成。我在complete接口里会先查会话状态已经是DONE就直接返回成功不会重新合并。这个判断虽然简单但少了它生产环境就会出现重复合并导致文件出错的严重问题。5. 内存、线程与性能调优让G级文件上传不拖垮JVM5.1 流式处理是底线别把文件读进内存我在评审很多同事的代码时发现一个高频问题拿到MultipartFile之后第一行代码是byte[] bytes file.getBytes()。这个写法在文件几百KB时没问题但当分片是64MB时就会对JVM堆内存产生明显压力如果前端没按分片传直接把整个文件打过来2GB的byte数组直接OOM。正确的做法是全程流式处理。分片落盘时用InputStream流式写文件计算MD5时用DigestInputStream边读边算不要先读进内存再统一操作。这一点是我反复强调的重点// 错误示范先读全部字节 byte[] data file.getBytes(); String md5 DigestUtils.md5DigestAsHex(data); // 正确示范边读边算 try (InputStream in file.getInputStream()) { DigestInputStream digestIn new DigestInputStream(in, MessageDigest.getInstance(MD5)); Files.copy(digestIn, chunkPath, StandardCopyOption.REPLACE_EXISTING); String md5 DigestUtils.md5DigestAsHex(digestIn.getMessageDigest().digest()); }DigestInputStream这个类很多人没用过它能在读文件流的同时自动更新摘要读完文件摘要也算完了性能开销极小而且是零额外内存开销。5.2 合并时的缓冲区与零拷贝优化合并大文件时缓冲区大小的选择会直接影响吞吐。如果用手动读写的方案缓冲区太小比如4KB会导致系统调用次数过多性能极差缓冲区太大比如几十MB又会对堆内存造成压力。理想的方案还是FileChannel.transferFrom它不经过用户态缓冲区由操作系统直接管理数据搬运。这种方式既不需要你手动分配缓冲区也基本不受JVM堆大小限制是合并G级文件的首选方案。如果因为某些原因比如文件系统或JDK版本限制用不了零拷贝退而求其次的方案是使用BufferedInputStream和BufferedOutputStream缓冲区大小设置在1MB左右。我在测试环境验证过1MB缓冲区比8KB缓冲区在合并吞吐量上能有数倍提升。5.3 线程池配置IO密集场景的参数怎么调分片上传是典型的IO密集型场景线程池配置不能随意为之。我见过有人用无界线程池高峰期几千个线程同时打上传接口直接把数据库连接池打爆。这里给出一个我实践中比较稳的配置思路核心线程数 CPU核数 × 2 最大线程数 核心线程数 × 2 队列容量 根据分片并发数预留一般几百就够 拒绝策略 CallerRunsPolicy让提交线程自己执行天然限流同时建议对上传接口做单独的Semaphore信号量控制比如设置最大并发上传数为50。信号量机制比线程池更灵活因为它只控制并发数不额外占用线程资源private final Semaphore uploadLimiter new Semaphore(50); public void uploadChunk(...) { if (!uploadLimiter.tryAcquire(3, TimeUnit.SECONDS)) { throw new BusinessException(上传并发过高请稍后重试); } try { // 落盘逻辑 } finally { uploadLimiter.release(); } }5.4 连接池与HTTP客户端别让应用层限制吞吐浏览器端并发上传分片时每个分片请求都是独立的HTTP连接。如果前端使用了HTTP/1.1同一个域名下浏览器默认限制6个并发连接这其实是好事避免前端把带宽占满。后端如果有异步回调要注意HTTP客户端连接池的配置最大连接数 200 每个路由的最大连接数 100 连接空闲超时 30秒在银行内网中间设备多、链路长连接池的keep-alive配置非常关键。连接复用能大幅降低TLS握手和TCP建连的开销。我见过有些系统上传性能差不是带宽不够而是每次请求都新建连接大部分时间都花在握手上了。6. 安全合规与审计银行级上传的额外功课6.1 目录穿越与文件名清洗上传功能是安全重灾区这一点在银行评审时更是被重点拷问。首先要防的就是路径穿越攻击。前端传过来的原始文件名可能包含../../、..\这类路径穿越字符如果后端直接用文件名拼接存储路径攻击者可以把文件写到任意目录甚至覆盖系统文件。我的处理方式private String sanitizeFileName(String originalName) { String name Paths.get(originalName).getFileName().toString(); return name.replaceAll([^a-zA-Z0-9._\\-\\u4e00-\\u9fa5], _); }还有一点存储路径不要用原始文件名做目录名用uploadId做目录名最安全。这样每个上传会话的文件都在一个以UUID命名的独立目录里既隔离了不同文件也彻底规避了路径穿越风险。6.2 文件类型白名单别信扩展名信文件头银行上传的文件类型通常是有限制的比如只允许PDF、ZIP、图片等几种格式。静态判断扩展名不靠谱一个恶意的EXE文件可以轻松改名为.jpg上传。正确做法是读取文件的magic number进行判断也就是文件头的固定字节序列文件类型文件头十六进制JPEGFFD8FFPDF25504446ZIP504B0304PNG89504E47代码示例public static FileType detectFileType(InputStream in) throws IOException { byte[] header new byte[4]; int read in.read(header); if (read 4) { return FileType.UNKNOWN; } if ((header[0] 0xFF) 0xFF (header[1] 0xFF) 0xD8) return FileType.JPEG; if (header[0] P header[1] K) return FileType.ZIP; if (header[0] % header[1] P header[2] D header[3] F) return FileType.PDF; return FileType.UNKNOWN; }这个校验要在分片合并完成后对完整文件做因为文件头在第一个分片里单看分片意义不大。6.3 病毒扫描与内容安全银行场景下的上传文件尤其是来自外部渠道的文件合并后必须经过病毒扫描才能进入业务流程。实现上有两个时机合并完成后立即扫描文件已完整落盘交给病毒扫描服务如ClamAV或商业防病毒网关检查扫描通过后才将文件状态置为ACTIVE业务系统才能读取扫描期间文件隔离存储扫描完成前放在隔离区目录扫描通过后移动到正式文件目录这个流程需要和文件状态机配合。我在设计时把文件状态分成了UPLOADED - SCANNING - ACTIVE - ARCHIVED几个阶段业务系统只认ACTIVE状态的文件天然规避了文件未扫完就被业务读取的风险。6.4 操作审计每一步都要留痕银行系统对可追溯性的要求极高无论谁在什么时候上传了文件、传了多少分片每一步都要有审计记录。我在数据库里专门设计了审计日志表记录这些字段操作人从统一认证平台获取不信任前端传的userId操作时间上传会话ID原始文件名和文件大小本次操作类型创建会话/上传分片/合并/删除分片序号和分片MD5来源IP从网关透传头中获取最终文件的存储路径和SHA256值审计日志写的是独立表和应用状态表分开避免审计写入影响主流程性能。同时配置了日志的定期归档满足银行的数据保留要求。7. 实测踩坑清单与参数推荐7.1 我实际踩过的五个坑这个方案的开发过程绝对不是一帆风顺的拉出我在测试环境和生产环境踩过的坑给各位做个参考。坑一分片太大导致负载均衡413。最初我把分片大小设成了128MB想着减少请求数量。结果测试环境的前端负载均衡配置的client_max_body_size只有100MB所有大分片都被拒了。后面我做了两件事把分片大小在配置中心调整为64MB并和运维确认了各层设备对请求体大小的限制范围。上线前一定要做一次中间设备限制摸底这个动作能让后续省很多事。坑二MD5校验时把整个分片读进了内存。我最初写分片校验时图省事用了file.getBytes()然后算MD5。64MB分片还好但测试跑到并发16个分片时JVM堆直接飙到2GB。后来改成DigestInputStream流式计算内存占用立刻降下来了。这个教训让我意识到上传服务的性能瓶颈往往不在CPU而在内存管理。坑三并发合并时文件顺序错乱。有段时间测试反馈合并出来的文件偶尔损坏排查下来发现是合并逻辑里用了并行流处理分片多个分片同时写入合并文件顺序全乱了。文件合并必须是严格串行的我在代码里加了一个并发控制确保同一会话的合并操作只有一个线程在执行问题解决。坑四数据库连接池被打满。高峰期多个大文件同时上传时分片状态写入太频繁几百个并发上传线程同时操作数据库连接池很快耗尽。处理方式是合并批量写入把分片状态先放内存队列凑够一定数量或固定间隔再批量write到数据库。性能问题缓解得非常明显。坑五complete请求超时。早期设计让complete接口同步等待合并完成再返回。文件大时合并要几十秒中间设备等不了直接断连接。后来改成异步合并complete接口只更新状态、触发合并线程立即返回合并中前端通过status接口轮询结果。这个改动之后接口超时问题从根源上消失了。7.2 关键参数推荐配置基于这些踩坑经验这里给出一份可以直接参考的参数配置表参数推荐值说明分片大小内网64MB银行内网带宽充足可以大一点分片大小外网8-16MB带宽不稳定分片要小前端并发数3-5避免占满带宽影响其他业务后端最大并发上传数50用Semaphore控制合并方式FileChannel零拷贝高性能首选线程池核心线程数CPU核数×2IO密集场景日志保留周期按审计要求至少1年以上银行审计要求严格7.3 上线前必须做的验证清单最后给一份上线前的自查清单都是我实际验证过有价值的点确认从浏览器到服务端的每一层设备都不限制请求体大小或者限制值大于分片大小测试断网恢复后的断点续传确认已传分片能被正确跳过并发10个以上文件同时上传观察数据库连接数和JVM内存曲线是否平稳合并完成后删除一个分片文件确认合并接口能触发补偿或报错而不是生成损坏文件验证同一分片重复上传的幂等性大文件合并完成后人工比对源文件和合并后文件的MD5是否一致这是最终保险8. 最后的补充做好监控再上线做上传功能最容易忽略的是运行监控。G级文件上传的耗时动辄几分钟到十几分钟如果服务端出了问题前端用户不会立即察觉只有等到超时才会抱怨。所以上线前必须把监控体系建好。我在实践中主要监控这几个指标当前上传中的会话数、合并中的大文件数量和大小、磁盘剩余空间、JVM堆内存使用率、分片上传的成功率和平均耗时。磁盘剩余空间这个指标尤其重要多个G级文件同时合并时磁盘空间下降的速度非常快一旦磁盘写满所有上传会话都会失败影响面很大。日志方面上传会话的全链路日志要打上uploadId这样单个文件从创建会话到合并完成的全过程都能串起来排查。我在项目里用MDC把uploadId放进日志上下文所有相关日志自动带上这个标识排查问题时一条命令就能拉出全部日志。我的真实体会是分块上传这个方案原理不复杂难的是把细节抠到位。文件越大细节的权重越高。一个不起眼的缓冲区设置、一个没处理好的并发边界在G级文件面前都会被放大成严重故障。但只要把会话管理、状态机、幂等、零拷贝、校验、监控这几个核心模块做扎实整套系统跑起来之后会非常稳定。希望这篇笔记能帮你少走一些我走过的弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑