尧图精选

SpringMVC实现DICOM大文件秒传断点恢复的分块上传方案

🕒 发布时间:2026/10/2 19:27:24 📁 来源:尧图网络
在医疗信息化项目里上传文件从来不是“选个文件、点提交”这么简单尤其是DICOM影像。一次CT序列动辄几百MB一台设备的增强扫描原始数据可以轻松超过2GB而很多医院网络环境并不是专线客户端和服务器之间的网络质量、带宽、稳定性都不可控。这两年我在做区域医疗影像平台时被这个“大文件上传”反复折磨过普通上传超时、传输中断后要重新传、服务端处理大请求时内存直接爆掉。后来把SpringMVC和几个上传组件搭配起来做了一套支持秒传、断点恢复的分块上传方案算是把这个问题彻底解决了。这篇文章就围绕这套方案的选型、接口设计、状态管理和那句“秒传”背后的原理展开希望能帮到正在做医疗系统或类似大文件传输场景的同学。1. 医疗影像传输的痛点与需求拆解1.1 为什么DICOM大文件的传统上传方式必然失败很多人第一次接触DICOM文件时会以为它跟普通图片差不多顶多就是几百KB。实际上DICOM序列是按“患者—检查—序列—实例”四级结构组织的一个检查可能包含上千个DICOM实例文件每个实例除了像素数据还带有大量标签信息再加上多期增强扫描整体体积非常可观。我在项目里统计过一个典型的上腹部增强CT检查压缩前的DICOM原始数据在800MB到1.5GB之间而乳腺断层合成、血管造影这类高分辨率序列单次检查轻松突破3GB。传统的方式也就是一个form表单加一个大的MultipartFile直接提交在这么大体量下面会有三个硬伤第一HTTP请求超时。SpringMVC默认的MultipartResolver处理完整个请求体才算结束文件越大请求保持时间越长一旦遇到代理服务器、网关、负载均衡设备的超时阈值请求就被拦腰截断报一个连接重置。第二服务器内存压力。如果直接用RequestParam(file) MultipartFile file接收Spring在请求解析阶段会把整个文件缓存到内存或临时文件中。虽然Spring会对超过max-upload-size的文件转写到磁盘上的temp目录但并发上来后大量的临时文件写盘、删除、IO竞争会拖垮整个应用的性能。我见过生产环境因为在并发上传时没控制好临时目录空间直接把系统盘写满的案例。第三没有断点概念。网络抖动是常态而不是意外一旦中断客户端只能从头再来。患者正躺在扫描床上等着影像上传到PACS技师那边急得不行这边却要重新传半小时这在业务上是不能接受的。所以这套方案的核心目标不是“把文件传上去”而是在弱网环境下也能把大文件传完重传成本接近零且不能拖垮服务器。1.2 “秒传断点恢复”要解决的三类问题拆开标题里的几个词其实是三类完全不同的问题秒传文件在服务器上已经存在了也就是别的患者或者同一次检查里已经传过相同的实例直接跳过传输返回一个“假上传成功”的结果。断点恢复传输过程中断后不需要重新上传整个文件只需要把缺失的部分补齐。快传虽然文件不存在、也没传过但通过分块并行上传把大文件拆成多个小块并行提交充分利用带宽缩短总体耗时。很多文章把这三个概念混在一起讲导致做出来的系统很拧巴。我在设计的时候就理清了秒传依靠的是文件指纹匹配断点恢复依靠的是分块状态记录快传依靠的是分块上传机制。三者可以独立存在但组合起来才是完整的方案。2. 组件选型与整体架构设计2.1 核心组件清单与分工网上常说的“组件”在这个场景里不是一个单独的jar包能搞定的。我最终选定的组合是层级组件职责前端上传组件web Uploader / 自研分块插件文件切片、并发控制、进度展示、断点状态查询后端上传组件SpringMVC MultipartResolver 自研ChunkController接收分块、落盘、块状态登记数据存储Redis MySQL块状态临时记录、文件登记信息持久化文件存储本地磁盘 / 共享存储 / 对象存储DICOM文件和临时块的存放状态协调分布式锁Redis示例防止多个块合并时重复合并或冲突这里要说明一下很多人一上来就推OSS分片上传组件我也试过但医疗行业的影像系统大多部署在内网甚至有些医院要求数据不出院区根本不能用公有云对象存储。所以最终方案是基于SpringMVC自身的multipart处理能力再配合Redis做状态登记自己封装了一套分块上传逻辑。前端用Web Uploader是因为它对并发分块、失败重试、MD5计算有现成方案不需要自己从零手搓一套前端切片逻辑。2.2 上传流程时序与状态模型整套流程可以分成三个阶段预检、分块上传、合并。第一阶段客户端读取文件的MD5值和总大小调用预检接口/check。后端对比文件指纹登记表如果发现同样大小、同样MD5的文件记录存在而且文件状态是“已合并”就直接返回秒传成功。如果存在记录但状态是“未完成”就返回已上传的块编号集合前端据此跳过这些块只上传缺失部分这就是断点恢复。第二阶段前端把文件按固定块大小切分通常我用1MB到8MB不等并发数控制在3到5路。每一块带上全局唯一的上传凭证md5和块序号提交到/chunk接口后端落盘后往Redis的位图里标记这一块已上传。第三阶段所有块上传完成后前端调用/merge接口。后端在拿到合并请求后先校验块数量和大小再按顺序把临时块合并成完整的DICOM文件最后再一次计算整个文件的MD5与客户端上报的MD5比对一致才登记完成状态。这里的关键是合并后的MD5校验不能省。状态机的流转是这样的CREATED - UPLOADING - MERGING - DONE异常情况下回退到UPLOADING也就是哪块缺了就传哪块。3. SpringMVC后端分块上传接口的实现细节3.1 分块接收的Multipart处理机制SpringMVC对文件上传的支持底层是MultipartResolver。在Servlet 3.0环境下直接配置StandardServletMultipartResolver即可关键是multipart-config的参数不能照搬默认值。我在web.xml或配置类里的设置是这样的multipart-config max-file-size10485760/max-file-size max-request-size10485760/max-request-size file-size-threshold1048576/file-size-threshold /multipart-config对应到分块场景每个chunk请求里只有一个块文件所以max-file-size设置为单个块的上限即可。我用的是5MB一个块所以这个值设置成10MB是合理的。这里容易踩的坑是max-request-size不能设置成整个大文件的尺寸因为分块上传每次都只提交一个小块如果把它设成10GB会直接导致所有上传请求被拒绝或者导致Tomcat在解析阶段就把整个请求读入内存。接收分块的核心Controller是这样的RestController RequestMapping(/pacs/upload) public class ChunkUploadController { PostMapping(/chunk) public ResultVoid uploadChunk( RequestParam(file) MultipartFile file, RequestParam(identifier) String identifier, RequestParam(chunkNumber) Integer chunkNumber, RequestParam(chunkSize) Long chunkSize, RequestParam(totalChunks) Integer totalChunks, RequestParam(totalSize) Long totalSize) throws IOException { // identifier 就是文件的MD5也是临时目录的命名空间 String chunkDir Paths.get(uploadRoot, identifier).toString(); File dir new File(chunkDir); if (!dir.exists()) { dir.mkdirs(); } // 块文件命名以chunkNumber为文件名方便合并时直接按序读取 File chunkFile new File(chunkDir, chunkNumber.toString()); // 防止块文件被重复写入覆盖 if (chunkFile.exists()) { return Result.success(chunk already exists); } file.transferTo(chunkFile); // 在Redis位图记录块状态 redisTemplate.opsForValue().setBit(pacs:chunks: identifier, chunkNumber, true); // 记录上传上下文后续合并要用 uploadContextService.record(identifier, totalSize, totalChunks); return Result.success(); } }这里有几个细节值得展开。第一个细节是块文件名的设计。我直接用chunkNumber作为文件名合并的时候用一个循环按0,1,2...totalChunks-1的顺序逐块写出不需要再额外保存“块顺序”这样的映射关系。如果像某些方案那样用UUID随机命名块文件合并时还得额外做一次排序或记录映射表纯属自己给自己添麻烦。第二个细节是file.transferTo(chunkFile)。这个方法在处理小文件时效率没问题但如果块大小为5MB且并发数高频繁transferTo会导致临时文件频繁创建和删除。我实测后发现transferTo在底层其实是把Spring缓存到磁盘上的临时文件移动或拷贝到目标位置性能瓶颈不在代码而在磁盘IO。所以后续我做了个优化把分块直接写到内存映射区或采用异步刷盘但那是另一个话题了后面再细说。第三个细节是幂等保护。chunkFile.exists()的判断看起来简单却能避免很多问题。在网络重试场景下同一个块可能被提交两次如果没有这个判断第二次提交就会覆盖第一次已经写好的块文件。加上判断后重复提交直接忽略前端在断点续传时扫描到“已上传”的块也不会重新传。3.2 分块合并顺序与文件完整性校验合并接口是整个上传链路的最后一环也是最容易出问题的一环。PostMapping(/merge) public ResultVoid merge(RequestParam(identifier) String identifier, RequestParam(fileName) String fileName) throws IOException { // 防并发合并同一identifier同时只能有一个合并任务 String lockKey pacs:merge:lock: identifier; Boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofMinutes(5)); if (!Boolean.TRUE.equals(locked)) { return Result.error(merge already in progress); } try { UploadContext ctx uploadContextService.get(identifier); if (ctx null) { return Result.error(upload context not found); } String chunkDir Paths.get(uploadRoot, identifier).toString(); // 合并前完整性检查所有块文件必须存在且大小一致 for (int i 0; i ctx.getTotalChunks(); i) { File f new File(chunkDir, String.valueOf(i)); if (!f.exists() || f.length() ! calculateChunkSize(i, ctx.getTotalSize())) { return Result.error(missing chunk i); } } // 创建最终输出文件 File target new File(finalRoot, generateStoreName(identifier, fileName)); try (FileOutputStream fos new FileOutputStream(target); FileChannel outChannel fos.getChannel()) { for (int i 0; i ctx.getTotalChunks(); i) { try (FileInputStream fis new FileInputStream(new File(chunkDir, String.valueOf(i))); FileChannel inChannel fis.getChannel()) { inChannel.transferTo(0, inChannel.size(), outChannel); } } } // 合并完成后计算最终文件的MD5与客户端上报值比对 String mergedMd5 DigestUtils.md5Hex(new FileInputStream(target)); if (!mergedMd5.equalsIgnoreCase(identifier)) { target.delete(); return Result.error(md5 mismatch after merge); } // 删除临时块目录 org.apache.commons.io.FileUtils.deleteDirectory(new File(chunkDir)); // 更新文件登记表状态 fileRecordService.markDone(identifier, target.getAbsolutePath(), fileName, ctx.getTotalSize()); return Result.success(); } finally { redisTemplate.delete(lockKey); } }这段代码里有几个值得注意的设计点。合并的顺序问题FileChannel.transferTo从0传输到inChannel.size()意味着按照块顺序偏移量自动累加不用手工维护“当前写入偏移量”。如果你用OutputStream逐个块write也没问题但FileChannel的transferTo在底层可以利用sendfile系统调用如果目标通道是网络通道在本地文件合并场景下性能反而可能不如普通IO因为它主要优化的是“文件到网络”的数据路径。所以这里用FileChannel倒不是追求极致性能而是代码写起来更干净不容易出错。合并前的完整性检查我在循环里判断每个块文件的大小是否等于calculateChunkSize(i, ctx.getTotalSize())这个函数要处理一个边界情况最后一个块通常比前面的块小private long calculateChunkSize(int index, long totalSize) { long defaultChunkSize 5L * 1024 * 1024; if ((index 1) * defaultChunkSize totalSize) { return totalSize - index * defaultChunkSize; } return defaultChunkSize; }这个检查能拦住一半以上的脏数据问题。比如客户端切块的时候因为网络卡顿某个块只传了一半就报“传输完成”或者某块文件被误删如果没有预检查合并出来的文件大小肯定不对最终MD5比对也会失败但那时候问题定位成本就高了。MD5比对合并完成后再算一次整个文件的MD5跟客户端上报的identifier比对。这一步看似浪费CPU实际上是最强的完整性保障。我见过太多只靠块大小校验就判定成功的方案结果文件合并后打不开DICOM解析器直接报错。医疗影像文件出现字节级别的损坏是完全不能接受的因为DICOM的某些关键标签如果损坏可能导致整个检查图像无法重建所以MD5比对必须做。3.3 断点记录表设计与管理分块上传必须有地方记状态我把状态分了两层第一层是MySQL的dicom_upload_record表负责业务层的持久化字段类型说明idbigint主键file_md5varchar(64)文件全量MD5file_namevarchar(255)原始文件名file_pathvarchar(512)合并后的存储路径total_sizebigint总大小total_chunksint总块数uploaded_chunkstext已上传块编号JSON数组兼容旧逻辑statusvarchar(20)CREATED / UPLOADING / DONE / FAILEDcreate_timedatetime创建时间update_timedatetime更新时间第二层是Redis的位图负责短周期内的高效查询// 记录某一块已上传 redisTemplate.opsForValue().setBit(pacs:chunks: identifier, chunkNumber, true); // 查询所有已上传块 ListLong uploadedChunks new ArrayList(); byte[] bitmap redisTemplate.opsForValue().get(pacs:chunks: identifier); if (bitmap ! null) { for (int i 0; i totalChunks; i) { if (isBitSet(bitmap, i)) { uploadedChunks.add((long) i); } } }为什么要Redis位图而不是直接在MySQL里存一个JSON数组因为大文件分块数量多一个1GB的文件按1MB块大小切分也有1024个块。如果每次上传一块就去MySQL里读JSON数组、改、再写回会带来巨大的写入放大。Redis位图天生就是为这种密集的布尔状态设计的每个块只占一个bit1024个块也就128字节而且setBit操作是O(1)的并发更新同一张位图也没有性能问题。考虑到系统可能重启、Redis可能宕机我在uploadContextService.record里做了一个定时补偿策略每次上传一块异步更新MySQL表里的uploaded_chunks字段这样即使Redis数据丢失客户端重新发起预检时还能从MySQL拿到大致的已上传块集合。这个补偿不是实时的但足够用了。4. 秒传原理MD5指纹判断与存储策略4.1 MD5计算的坑与性能优化秒传的前提是快速拿到文件指纹。前端计算MD5的方案有很多市面上的上传组件通常内置spark-md5可以增量计算也就是边读取文件边更新哈希状态避免一次性把整个文件读入内存。但MD5计算本身是有成本的吗一个1GB的文件在前端用spark-md5计算实际耗时大约在2到5秒具体取决于浏览器所在机器的CPU和磁盘读取速度。很多文章把秒传吹得神乎其神其实计算MD5的时间并没有被省略只是从前端交互感上“感受不到上传过程”。在医疗场景里还有一个更隐蔽的问题DICOM文件往往不是单个大文件而是一个序列里几百个小文件。如果对每个文件都走一遍完整的MD5计算加秒传检测整体耗时依然可观。我在前端做了个优化策略对于小于5MB的文件直接走普通上传不做秒传检测只有超过5MB的文件才进入分块秒传流程。理由是秒传检测本身也有网络开销一次HTTP请求小文件走秒传反而更慢。如果客户端是原生程序而非浏览器比如医院里的DICOM网关计算MD5可以放到独立线程里同时把文件分块上传给并发了这样MD5算完的同时块也传得差不多了最后在合并阶段用正式算出的MD5覆盖初始值即可。但浏览器场景下没法这么干所以前端必须把MD5计算作为一个独立的前置步骤。4.2 秒传命中后的处理逻辑预检接口的实现很简单但设计上要考虑“假命中”的问题。GetMapping(/check) public ResultCheckResult check(RequestParam(md5) String md5, RequestParam(size) Long size, RequestParam(fileName) String fileName) { // 查询同MD5、同大小的文件记录 DicomUploadRecord record fileRecordService.findByMd5AndSize(md5, size); if (record null) { return Result.success(new CheckResult(false, Collections.emptyList(), 0)); } if (DONE.equals(record.getStatus())) { // 文件已合并完成直接秒传 // 这里还可以做一步物理存在性检查防止记录存在但文件被清理 File f new File(record.getFilePath()); if (f.exists() f.length() size) { return Result.success(new CheckResult(true, Collections.emptyList(), record.getTotalChunks())); } // 物理文件不存在走重新上传并修正记录 fileRecordService.markFailed(record.getId()); return Result.success(new CheckResult(false, Collections.emptyList(), 0)); } else { // 未完成返回已上传的块集合供断点续传 ListLong uploaded getUploadedChunks(md5, record.getTotalChunks()); return Result.success(new CheckResult(true, uploaded, record.getTotalChunks())); } }这个接口能在三种请求下给出正确响应全新文件返回shouldUploadtrue, uploadedChunks[]未完成文件返回shouldUploadfalse, uploadedChunks[0,1,2,5,...]已完成文件返回秒传命中前端直接展示上传成功这里的关键是物理存在性检查。我吃过一次亏文件登记表里状态是DONE但磁盘上的文件因为存储清理任务被误删了客户端秒传成功医生在PACS端却拉不到图像。后来我在所有返回“秒传成功”的逻辑里都加了f.exists() f.length() size校验哪怕多一次磁盘访问也值得。还有一个坑是MD5碰撞的问题。严格来说MD5已经不算安全哈希两个不同文件有可能碰撞出同一个MD5。但在医疗内网环境发挥不了攻击性作用且为了实现秒传要完整读取几十GB的所有已存文件来计算更安全的SHA256代价更高。折中方案是判定命中后额外比较文件大小。如果MD5和大小都一致基本可以认为是同一文件。如果项目对安全性要求极高可以把MD5升级为MD5文件大小再加上文件名匹配进一步降低误判概率。5. 断点续传的完整链路与前端配合5.1 断点状态查询接口设计断点续传的核心依赖是“客户端知道哪些块已经传过”。我的预检接口已经能返回uploadedChunks数组前端拿到这个数组就能在分块上传循环里跳过对应块。不过这里有个体验层的问题是如果网络在断连后重启前端并不知道当前传输任务到底中断在哪一块。所以前端的断点恢复逻辑不能只依赖于“服务端存在的块”还需要维护一个本地的进度索引。我用的是本地localStorage 文件MD5双键索引的方式// 保存进度 function saveProgress(md5, uploadedChunksSet) { localStorage.setItem(upload_progress_ md5, JSON.stringify([...uploadedChunksSet])); } // 恢复进度 function loadProgress(md5) { const raw localStorage.getItem(upload_progress_ md5); if (raw) { return new Set(JSON.parse(raw)); } return null; }在恢复流程里优先使用本地进度同时调用预检接口以服务端状态为准对两者取并集这样既能在网络完全没断过的情况下直接跳过已传块也能在本地进度丢失时回退到服务端状态。5.2 前端分块组件的配合要点前端负责切片、并发控制、进度展示。我用的Web Uploader配置如下const uploader WebUploader.create({ swf: /js/Uploader.swf, server: /pacs/upload/chunk, pick: #picker, accept: { title: DICOM, extensions: dcm }, chunked: true, // 每个chunk大小和后台的默认分块大小保持一致 chunkSize: 5 * 1024 * 1024, // 并发上传数3~5为宜太大容易打满带宽并导致服务端临时文件压力过大 threads: 3, // 文件唯一标识使用文件的MD5秒传和断点续传都依赖它 formData: { identifier: function(file) { return file.md5; } }, // 计算文件MD5启用后在上传前先计算 prepareNextFile: true, auto: false });chunkSize这个参数前后端必须严格一致。如果前端切了10MB的块后端按5MB的块大小做完整性检查合并时就会出现块大小不匹配。我用的是固定值因为我做过分块大小动态调整的实验结论是动态调整会引入大量边界问题尤其在断点恢复时旧块和新块的大小不一致会让合并逻辑变得极其复杂。并发数的选择threads: 3是相对保守的值。在医院内网环境中网络带宽普遍在100Mbps到1Gbps之间5个并发块实际上已经能打满带宽了。并发过高会导致单台DICOM工作站的上传任务占满所有网络资源影响其他临床应用这在真实的PACS环境里是会挨骂的。处于断点恢复的场景时前端会根据预检接口返回的uploadedChunks在建队时直接剔除这些块uploader.on(uploadBeforeSend, function (block, data) { // 若该块已在服务端存在直接跳过 if (uploadedChunkSet.has(block.chunk)) { return false; } data.chunkNumber block.chunk; data.totalChunks block.file.__chunks; data.totalSize block.file.size; });另外所有上传组件都需要开启“失败重试”能力。Web Uploader在默认情况下网络错误会自动重试但对我的场景来说更关键的是在浏览器刷新后恢复任务。这里没有银弹只能靠本地进度存储和预检接口组合实现具体做法就是刷新后重新创建上传组件先查出已上传的块再继续传。6. 踩坑实录与性能优化6.1 并发合并与文件锁问题合并接口刚上线时遇到一个线上事故同一个文件因为某种原因两个客户端同时发起合并请求结果两个合并任务都检测到块齐全都开始合并最终生成了两个同名文件其中一个覆盖了另一个导致MD5校验失败而且因为并发写入同一个文件其中一个合并任务写到一半时文件被另一个任务截断了。解决方式就是在合并前加分布式锁。我用的RedisSETNX 过期时间的方案也就是在合并接口开头加setIfAbsent合并完释放锁。注意这里不能用简单的JVM锁因为服务是多节点部署的两个请求可能落在不同的节点上JVM锁无法跨节点生效。6.2 内存溢出与临时文件清理分块上传把单文件请求拆小了但带来了新的内存问题SpringMVC在解析multipart请求时如果文件小于file-size-threshold会直接读入内存如果超过阈值则写入临时目录。默认情况下Tomcat的临时目录是系统/tmp而且不会自动清理。我在生产环境配置了一个专门的上传临时目录并在系统层面加了一个定时清理任务清理超过24小时未合并的临时块目录。要注意一点临时清理绝不能简单按“目录修改时间”来删因为分块上传过程中会不断修改目录下的块文件修改时间一直在变。我采用的方式是在Redis里给每个上传任务设置一个过期时间比如2小时每上传一个块就刷新这个过期时间清理任务扫描Redis中已过期的上传任务删除对应的临时块目录。// 上传块时刷新过期时间 redisTemplate.expire(pacs:chunks: identifier, Duration.ofHours(2)); redisTemplate.expire(pacs:upload:ctx: identifier, Duration.ofHours(2)); // 定时清理任务每10分钟扫描一次 public void cleanStaleUploads() { SetString keys redisTemplate.keys(pacs:chunks:*); for (String key : keys) { Long ttl redisTemplate.getExpire(key); if (ttl ! null ttl 0) { String identifier key.replace(pacs:chunks:, ); FileUtils.deleteQuietly(new File(uploadRoot File.separator identifier)); } } }这个方案还解决了另一个问题上传过程中用户突然关闭浏览器临时块永远不会合并如果只靠MySQL表状态这个脏数据会在表里留存很久。Redis的过期机制能自动完成这部分清理。6.3 实际压测数据与优化效果最后列一组实测数据环境是4核8G的应用服务器千兆内网客户端为医院工作站在线浏览器测试文件为1.2GB的DICOM序列压缩包。方案总耗时是否支持断点服务端峰值内存传统单文件上传约120秒且极易超时否约2GB几乎OOM分块上传未优化约45秒是但状态不准约800MB分块上传Redis位图并发3路MD5防重约28秒是真支持浏览器刷新恢复约350MB优化效果明显但这里要说清楚耗时下降不只是因为分块改变了“总量”而是并发3路充分利用了千兆带宽加上避免了单文件上传时Tomcat解析大文件带来的性能和内存损耗。还有一个容易被忽视的优化点DICOM文件通常包含大量重复的元数据标签比如患者姓名、检查号、设备编号等在多个文件中是完全相同的。如果这些文件都通过同一套上传通道传输压缩率会非常高。但医疗影像系统使用方往往不允许在传输层对DICOM文件做有损压缩怕影响后续诊断所以这个方案里我没有对文件内容做任何压缩只做了分块和断点管理。这也是影像文件秒传方案与普通网盘秒传方案的重要区别之一网盘可能用内容指纹加压缩双重手段而医疗系统必须在保存原始字节的前提下保证完整性和可追溯性。在我实际交付项目的过程中还有一个容易被业务方质疑的点秒传到底会不会导致“误判同一文件”比如两个患者的检查结果中如果恰好有一个相同的DICOM文件秒传会不会把第二个患者的数据错误地关联到第一个患者的记录上这个担心是合理的。所以在最终交付方案里我在文件记录表上加了sourceCheckId字段秒传命中时除了文件指纹匹配还要校验来源检查ID是否一致不一致则走普通上传避免文件跨检查被错误复用。这个设计在PACS系统对接时尤其关键因为影像数据与患者检查的关联关系一旦错位就是重大医疗事故风险。至此这一整套基于SpringMVC组件实现DICOM大文件秒传断点恢复的方案已经完整落地。在服务器上跑了大半年最深的体会是分块方案本身不复杂复杂度主要在于状态的可靠记录、边界条件的处理和恢复机制的闭环。把那几个预检、合并、清理接口打磨稳了剩下的交给时间去验证。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →