news 2026/10/13 4:09:56

SpringMVC大文件上传实战:分片上传、断点续传与秒传方案解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringMVC大文件上传实战:分片上传、断点续传与秒传方案解析

1. 混乱的项目现场:一次大附件上传引发的“血案”

大概每个做Java后端的人,都会被“文件上传”这件事恶心过几回。小文件还好,默认配置一开就能跑,但一旦文件体积从几MB跳到几百MB甚至几个GB,整个系统的脾气就全变了。

我印象特别深的一次,是某项目组做内部资料库系统,附件动辄三四百MB的CAD图纸和设计文档。上线第一周就炸了:运维反馈说服务器频繁内存告警,后来直接OOM,Tomcat进程被系统杀掉。当时负责这块的同事一脸懵,他配置的SpringMVC上传逻辑明明“看着没问题”——CommonsMultipartResolver是配了的,maxUploadSize也设了200MB,前端表单就是普普通通的multipart/form-data,点击提交,等就完事了。

结果呢?等来的不是成功提示,而是浏览器转圈转到超时,紧接着Nginx报413 Request Entity Too Large,后端日志里一串IOException: Connection reset by peer。更诡异的是偶尔能传上去几个小文件,但大文件每次必挂。

这不是个例。SpringMVC大附件上传这件事,很多团队是在生产环境被教育过之后才愿意认真思考方案的。今天这篇文章,我就把这个问题从头到尾拆开讲:为什么传统上传方式在大文件面前全线崩溃、分片上传到底该怎么设计、后端接口怎么落、断点续传和秒传怎么处理,以及部署阶段那些容易把人坑到怀疑人生的细节。

2. “传统三件套”为什么在大文件场景下全线溃败

2.1 CommonsMultipartResolver的“全量加载”逻辑

很多老项目用的是SpringMVC经典配置:CommonsMultipartResolver+MultipartFile+ 一个@RequestParam("file")。这套组合处理5MB以内的文件确实香——但是大文件场景下,它有一个致命的默认行为:Servlet容器解析multipart请求时,会把请求体整个读入内存。

具体点说,在Spring的配置里有一个隐藏开关:

<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!-- 上传文件总大小上限 --> <property name="maxUploadSize" value="209715200"/> <!-- 单个文件大小上限 --> <property name="maxUploadSizePerFile" value="209715200"/> <!-- 内存阈值,默认1KB --> <property name="maxInMemorySize" value="1024"/> </bean>

maxInMemorySize这个参数是很多人的盲区。它代表的是“文件在内存中缓冲的阈值”,默认只有1024字节。也就是说,超过1KB的文件会先落盘到临时目录,理论上大文件走的是磁盘临时文件路径,不至于直接整块塞内存。

但问题在于:临时文件仍然是一整个文件。SpringMVC的处理流程是先把整个multipart请求体完整解析完,把每个文件暂存到FileUpload的临时目录里,所有字段也全部解析到位后,才会进入你的Controller方法。在这个解析完成之前,整个请求的数据都以文件形式躺在磁盘上,Tomcat的worker线程则一直阻塞在等待解析上。假设用户上传一个500MB的文件,磁盘IO、CPU解析、内存拷贝几乎同时被拉满,如果放着不管,任何一个环节瓶颈都会导致请求超时或连接被重置。

2.2 被忽略的Tomcat与Nginx层限制

还有一层问题是请求体大小限制,很多人只调了Spring的配置,却不知道Tomcat和Nginx还有自己的闸门:

  • Tomcat从8.5版本开始,maxSwallowSize默认2MB,它决定的是连接器在请求体解析出错后“吞掉”剩余数据的最大字节数,超出的部分会直接导致连接关闭,前端表现为Connection reset by peer。
  • Nginx的client_max_body_size默认是1MB,超过就返回413。这几乎是所有人在“为什么我后端调大了还是传不上去”这个问题里第一个遇到的坑。
组件默认限制大文件常见异常表现
Nginxclient_max_body_size1MB413 Request Entity Too Large
TomcatmaxSwallowSize2MBIOException: Connection reset by peer
SpringmaxUploadSize无限制401/500,或OOM
浏览器超时无统一默认值上传进度长时间不动后超时

即便你把所有配置都调大,传统方案还有一个不可回避的硬伤:没有进度反馈机制。浏览器就是干等,用户根本不知道传了一半还是卡了。一旦网络抖动导致中断,又要从头再传。几百MB的文件让用户一遍遍重传,体验可想而知。

2.3 避坑小结:传统方案“三宗罪”

归纳下来,传统方式在大附件场景的失败有三个本质原因:

  1. 整体性依赖——整个文件必须完整传到服务器才进入业务逻辑,中间任何环节断了都得重来,没有“部分成功”的概念。
  2. 资源占用峰值高——解析期间内存、磁盘IO、Tomcat线程同时被一个请求长期占用,并发一上来直接拖垮节点。
  3. 链路限制层次多——Nginx、Tomcat、Spring三层配置各自为政,一个没调就是一片红,排查起来令人发狂。

所以,方案切换不是“升级配置”的问题,而是架构思路的转变:把“一次大传输”拆成“多次小传输”,把“整体成功”变成“分片可重试”。

3. 分片上传的核心设计:从文件到分片的映射逻辑

3.1 为什么分片是唯一务实的选择

市面上的成熟方案,无论是某云OSS的断点续传,还是各种网盘的上传,本质都是分片(Multipart Upload)。核心思路是:

  • 前端把大文件按固定大小切成若干分片,逐片上传。
  • 每一片都是一个独立的HTTP请求,失败只重传失败的那一片。
  • 所有分片传完后,后端按顺序合并成完整文件。

这个设计把一个大风险拆成了很多小风险,每个小风险都能单独恢复。而且单片体积小,单次请求耗时短,后端内存占用被压得很低,Tomcat线程也不会被长时间占用。

3.2 分片大小怎么定?一个掰开揉碎的计算

分片大小不是随便拍的。我一般建议在5MB到20MB之间,具体取决于你的网络带宽和并发策略。

给你一个实际算过的例子:假设目标文件是1GB,分片设为10MB,那就是102个分片。如果客户端上行带宽是10Mbps(约1.25MB/s实际速率),每片理论耗时8秒左右,102片串行跑完大约是13.6分钟。如果前端控制并发数为3,总耗时能压缩到5分钟左右。

但如果分片设成1MB,1024个分片会带来两个问题:一是HTTP请求数量太多,每次请求都有网络RTT和鉴权开销,反而拖慢整体速度;二是后端合并时打开、关闭文件句柄的次数成倍增加。分片设成50MB甚至100MB也可以,但单片传输时间变长,断点重试的粒度就变粗了,用户体验和容错性都会下降。

我个人的经验值是:10MB是通用性最好的分片大小。日活不大的内部系统用5MB也稳妥,带宽充足且文件普遍超大的场景可以提高到20MB。

3.3 前端必须处理的三个细节

分片上传虽然是后端主导,但前端的准备工作直接决定成败。

第一,读取文件分片。浏览器端用File.slice()方法即可,不需要读取整个文件进内存。有个反直觉的错误做法是用FileReader把整个文件读成base64再切,那会直接把浏览器内存撑爆。正确做法是只切分索引:

const FILE_CHUNK_SIZE = 10 * 1024 * 1024; // 10MB function createChunks(file) { const chunks = []; let start = 0; while (start < file.size) { const end = Math.min(start + FILE_CHUNK_SIZE, file.size); const blob = file.slice(start, end); chunks.push({ blob: blob, start: start, end: end, index: Math.floor(start / FILE_CHUNK_SIZE) }); start = end; } return chunks; }

blob对象在这个阶段只是引用原文件的一部分,不会复制数据,所以放心用。

第二,全局唯一标识。给文件生成一个唯一的fileId,建议基于内容计算MD5(后面详细说),也可以在前端生成UUID。这个ID是整个上传流程的“主键”,后续的断点续传、合并、进度记录全都要靠它。

第三,并发控制。多数浏览器对同一域名的并发连接数有限制,而且如果一次把几十个分片全发出去,后端会同时收到一大堆请求,很容易把服务打崩。保险做法是限制并发数在3到5,这里给一个简单的并发调度示例:

async function uploadChunks(chunks, fileId, concurrency = 3) { const queue = chunks.map(chunk => ({ blob: chunk.blob, index: chunk.index, fileId: fileId, fileName: fileNameValue })); const pool = []; for (let i = 0; i < concurrency; i++) { const worker = (async () => { while (queue.length > 0) { const item = queue.shift(); await uploadOne(item); } })(); pool.push(worker); } await Promise.all(pool); }

注意在实际工程里还要加上指数退避重试,避免网络抖动时连续失败导致任务整体卡死。

4. 后端接口与合并逻辑:一份可以直接落地的实现

4.1 接口清单与数据模型设计

分片上传后端至少要暴露三个接口:

接口用途关键参数
POST /api/file/upload/chunk上传单个分片fileId,chunkIndex,totalChunks,file
POST /api/file/upload/merge合并全部分片fileId,fileName,totalChunks
GET /api/file/upload/status查询已上传分片(断点续传用)fileId

设计数据模型时,建议用两张表:一张记录上传任务,一张记录分片完成状态。

上传任务表核心字段:

create table upload_task ( id bigint primary key auto_increment, file_id varchar(64) not null, file_name varchar(255) not null, total_size bigint not null, chunk_size bigint not null, total_chunks int not null, status tinyint default 0, -- 0新建 1上传中 2合并中 3完成 4失败 create_time datetime, update_time datetime, unique key uk_file_id (file_id) );

分片状态表核心字段:

create table upload_chunk ( id bigint primary key auto_increment, file_id varchar(64) not null, chunk_index int not null, chunk_size bigint not null, upload_time datetime, unique key uk_file_chunk (file_id, chunk_index) );

这张分片表是断点续传的核心依据:每次上传分片时先查它,如果该分片已经存在且大小一致,就跳过上传,从而避免重复传输。

4.2 分片上传接口的Controller实现

Controller的定义很简单,核心逻辑在Service层:

@RestController @RequestMapping("/api/file/upload") public class FileUploadController { @PostMapping("/chunk") public Result uploadChunk(@RequestParam("fileId") String fileId, @RequestParam("chunkIndex") Integer chunkIndex, @RequestParam("totalChunks") Integer totalChunks, @RequestParam("fileName") String fileName, @RequestParam("file") MultipartFile file) { uploadService.uploadChunk(fileId, chunkIndex, totalChunks, fileName, file); return Result.success(); } @PostMapping("/merge") public Result merge(@RequestBody MergeRequest request) { String filePath = uploadService.merge(request.getFileId(), request.getFileName(), request.getTotalChunks()); return Result.success(filePath); } }

这里要重点说明:分片上传接口收到的每一片,本质上仍是一个小文件,用MultipartFile接收完全没问题。前面全量上传的问题在于“一个请求里塞了整个文件”,分片后每个请求最多10MB,压力完全可控。

4.3 Service层的分片落盘与去重逻辑

Service层最关键的是分片落盘逻辑。推荐的做法是:每个分片单独存储在独立目录里。目录结构可以是{存储根目录}/{fileId}/{chunkIndex}.part。

@Service public class UploadService { @Autowired private UploadTaskMapper taskMapper; @Autowired private UploadChunkMapper chunkMapper; private String rootDir = "/data/file_storage"; public void uploadChunk(String fileId, Integer chunkIndex, Integer totalChunks, String fileName, MultipartFile file) throws IOException { // 1. 首次上传时初始化任务记录 if (chunkIndex == 0) { UploadTask task = new UploadTask(); task.setFileId(fileId); task.setFileName(fileName); task.setTotalChunks(totalChunks); task.setStatus(1); taskMapper.insert(task); } // 2. 分片去重:已存在尺寸一致的分片直接返回 UploadChunk exist = chunkMapper.selectByFileIdAndIndex(fileId, chunkIndex); if (exist != null && exist.getChunkSize().equals(file.getSize())) { return; } // 3. 写分片到磁盘 Path chunkPath = Paths.get(rootDir, fileId, chunkIndex + ".part"); Files.createDirectories(chunkPath.getParent()); try (InputStream in = file.getInputStream()) { Files.copy(in, chunkPath, StandardCopyOption.REPLACE_EXISTING); } // 4. 记录分片元数据 UploadChunk chunk = new UploadChunk(); chunk.setFileId(fileId); chunk.setChunkIndex(chunkIndex); chunk.setChunkSize(file.getSize()); chunkMapper.insertOrUpdate(chunk); } }

注意上面几个容易被忽略的细节:

  • 目录隔离:每个fileId一个目录,避免多个文件的分片混在一起。
  • 分片去重:这个判断非常关键。如果前端因重试导致同一分片重复提交,后端不用重复写磁盘,直接忽略。
  • 空文件边界:最后一个分片可能小于固定分片大小,所以不能默认假设每片都是10MB。我在代码里用file.getSize()记录实际大小,而不是用固定值,就是为了兼容末片。

4.4 合并的实现与边界处理

合并是整个流程的最后一个关键动作。逻辑上不复杂:按chunkIndex从小到大把所有分片按顺序写入一个目标文件。

public String merge(String fileId, String fileName, Integer totalChunks) throws IOException { Path dir = Paths.get(rootDir, fileId); Path target = Paths.get(rootDir, fileName + System.currentTimeMillis() + ".dat"); // 1. 校验分片完整性:有多少分片已经上传 List<UploadChunk> uploadedChunks = chunkMapper.selectByFileId(fileId); if (uploadedChunks.size() != totalChunks) { throw new IllegalStateException("分片不完整,已上传 " + uploadedChunks.size() + " / " + totalChunks); } // 2. 按索引排序后逐个写入 Set<Integer> existsIndex = uploadedChunks.stream() .map(UploadChunk::getChunkIndex).collect(Collectors.toSet()); try (FileChannel out = FileChannel.open(target, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) { for (int i = 0; i < totalChunks; i++) { if (!existsIndex.contains(i)) { throw new IllegalStateException("缺失分片: " + i); } Path chunkPath = dir.resolve(i + ".part"); try (FileChannel in = FileChannel.open(chunkPath, StandardOpenOption.READ)) { long size = in.size(); long transferred = 0; while (transferred < size) { transferred += in.transferTo(transferred, size - transferred, out); } } } } // 3. 清理临时分片目录 deleteDirectory(dir.toFile()); return target.toString(); }

合并逻辑里有一个值得展开的点:不要用普通的InputStream读取再写入,更不要用Files.readAllBytes()。大文件场景下这种搞法会大量占用内存。正确姿势是用FileChannel.transferTo(),它底层走的是零拷贝路径,数据不经过用户态缓冲,合并1GB文件的内存占用非常稳定。

另外,校验逻辑里“缺失分片直接抛异常”不是最优解。更友好的做法是返回缺失分片的索引列表,让前端知道还差哪几片、继续补传。这里为了简洁用了异常,在真实项目中我建议改成返回状态码+缺失列表。

4.5 一个隐蔽坑:同名文件冲突与秒传顺带解决

合并目标文件名如果直接用fileName,会引发两个后果:一是不同用户上传同名文件时互相覆盖;二是重复上传同一个文件会白白占用磁盘。正确的设计思路是存储层文件名与业务文件名分离——存储层用fileId或UUID命名,业务层在数据库里记录原始文件名和存储路径的映射关系。

而且这个设计天然支持“秒传”功能:如果前端在上传前先计算出文件MD5,后端在数据库里按MD5查一下,发现已经存在相同内容的文件,直接返回“已有文件地址”,前端连分片都不用传了。我后面单独讲这块。

5. 断点续传、秒传与并发合并时的状态一致性

5.1 断点续传不仅仅是个“前端功能”

断点续传经常被误解成“前端重新传一遍,服务器跳过已传分片”。但其实后端必须提供两个能力:

  1. 查询已上传分片状态——前端恢复上传时,需要知道哪几片不用重传。
  2. 对重复分片的幂等处理——即使前端判断失误重传了某个分片,后端也不能报错。

实现幂等的关键就是我上面写的“分片去重”逻辑:先查upload_chunk表,如果分片记录存在且大小一致,直接返回成功。这个设计能让前端无脑重试,大大简化了断点续传的复杂度。

5.2 秒传的MD5方案:先查后传

秒传的实现思路很简单:在正式上传前,前端先计算整个文件的MD5,然后调用一个检查接口,后端在文件记录表里按MD5查找。

@GetMapping("/check") public Result checkFile(@RequestParam("md5") String md5, @RequestParam("fileName") String fileName) { FileRecord record = fileRecordMapper.selectByMd5(md5); if (record != null) { return Result.success(record.getStorePath()); } return Result.fail("文件不存在"); }

如果返回了存储路径,前端直接跳过上传,用户体验就像“秒传”。

这里有两个实际工程里要特别留意的问题:

  • 前端MD5计算性能。一个2GB的文件在浏览器里用SparkMD5全量计算,可能需要好几十秒甚至几分钟。优化思路有两个:一是用File.slice分段增量计算,避免一次性读取;二是任务开始时就立即在后台线程算MD5,同时先把首个分片上传了,两者并行。不要为了算MD5把首次上传卡在原地。
  • 后端MD5查重可靠性。MD5本身有极小概率碰撞,对安全性要求极高的系统(比如涉密件、合同扫描件),建议再加一层“文件大小+MD5”双重校验,甚至用SHA-256。正常业务场景MD5已经够用。

5.3 合并时的并发问题:同一个fileId的并发合并请求

合并接口有一个隐蔽的并发隐患:如果前端网络抖动导致合并请求被重复提交,后端会同时执行两份合并逻辑。两份逻辑都读到了“分片完整”,同时写入目标文件,会导致目标文件损坏或写入异常。

解决方案是在合并接口上加锁。用分布式系统常用的基于Redis的分布式锁,或者简化版用文件系统的原子操作。单体应用用synchronized(fileId.intern())也能顶一阵:

private final Map<String, Object> lockMap = new ConcurrentHashMap<>(); public String merge(String fileId, String fileName, Integer totalChunks) throws IOException { Object lock = lockMap.computeIfAbsent(fileId, k -> new Object()); synchronized (lock) { // 合并逻辑 } lockMap.remove(fileId); }

注意lockMap要清理,否则文件一多内存就会泄漏。更稳妥的做法是设置过期时间或用Caffeine缓存管理。

5.4 合并中偶发的“分片不见了”排查

我在实际项目里遇到过一种诡异情况:合并时校验分片完整,但真正读文件的时候某个分片却找不到。定位到最后是分片清理线程和合并线程的竞争。如果后端设计了“定时清理未完成任务”的Job,这个Job可能会把尚在传输中但数据库状态还没更新的分片文件删掉。解决方案是在清理策略里留一个“最后活跃时间”的宽限期,比如超过2小时没有任何分片上传动作的任务才允许清理,并且清理前要锁定任务状态。

6. 部署层面的隐藏陷阱:上传目录、磁盘、JVM与监控

6.1 临时目录别放在/tmp

很多SpringMVC项目默认把上传临时文件放在/tmp下(Servlet容器的javax.servlet.context.tempdir)。Linux的/tmp目录有三个隐患:

  1. 系统重启会清空。如果系统恰好在你上传到一半时重启,所有已传分片直接蒸发。
  2. 分区空间有限。/tmp通常是根分区的一部分,跟操作系统共用空间。几百MB的分片文件堆起来,很容易把磁盘写满,导致服务器崩溃。
  3. 权限混乱。多用户服务器上如果权限设置不严,可能存在其他进程读写风险。

建议统一在系统规划里单独挂一个数据盘,比如/data/file_storage,并且权限收紧到只有应用账号可读写。另外整个分片目录最好是“纯本地磁盘”,不要用NFS网络盘,分片频繁读写时网络盘IO延迟高、稳定性差,一直在生产环境给我惹麻烦。

6.2 磁盘空间与文件清理策略

大文件上传服务器的磁盘管理是一门必修课。我见过太多服务器因为忘记清理临时文件而被塞爆的案例。

设计清理机制有三个原则:

  • 上传过程中不清理活动文件。通过记录每个任务的最新活跃时间,判断任务是否在推进。
  • 合并完成后立即清理分片目录。不要等到“定时Job”来收尾,合并成功的路径里直接同步删除最快。
  • 定期任务兜底清理僵尸任务。比如每小时扫描一次,找出超过30分钟没有任何分片更新的任务,删除分片目录,并把任务状态标记为失败。

这里再分享一个磁盘压力分摊的经验:上传高峰期,多分片同时写盘会让单块磁盘的IO队列非常深。如果系统规模稍大,建议把分片存储按fileId哈希分散到多个磁盘目录,比如/data/upload_shard_01、/data/upload_shard_02,用哈希值取模决定落在哪个盘。加个三行代码,就能显著降低单盘压力。

6.3 JVM与并发参数的配套调整

分片上传看似解决了“大请求一次性加载”的问题,但并发数上来了之后,JVM和线程池依然需要配套优化:

  • Tomcat最大工作线程数。默认maxThreads是200,假如每请求平均耗时1秒,200并发已经是不少了。但如果单片只有几MB,一个请求的CPU和IO开销都极低,一般不需要刻意调大。倒是进程句柄上限ulimit -n很重要,默认1024在多分片并发时很容易被耗尽。
  • 文件句柄泄漏。代码里如果每一个分片都打开InputStream而不关闭,一段时间后就会看到Too many open files。建议所有IO都使用try-with-resources。
  • 上传接口的限流。为了不让用户疯狂点重传把服务压垮,可以用令牌桶或简单的并发信号量限制全局并发上传数,比如Semaphore控制在50。

6.4 日志与监控:出问题时如何快速定位

整理分片上传的监控时,我最看重的四个指标:

指标作用
分片上传TPS判断当前上传压力
分片平均耗时判断网络和磁盘是否健康
失败分片数判断链路稳定性
磁盘剩余空间防止磁盘写满

日志方面,强烈建议在分片上传接口里记录fileId、chunkIndex、chunkSize、耗时四个字段。这样问题排查时可以精确到“某一个分片为什么慢”。我在实践中遇到过明明是同一个文件,前50片都很快,第51片突然耗时飙升的情况,排查后是磁盘因为其他业务写入导致瞬时IO拥塞。没有分片级日志,这种问题压根查不出来。

7. 一次完整排障实录:分片上传偶尔失败的那几片

最后分享一个真实的排障过程,过程比结论有参考价值。

某次上线后,有用户反馈“大文件上传有一定概率失败,报错说某一片上传超时”。同事一开始怀疑是网络问题,但重试后还是偶发失败,而且失败的分片索引随机。后来查后端日志,发现失败的请求全部卡在一个现象上:Request timed out,但看网络流量并没有异常。

继续往下挖,发现这批用户走的链路经过了Nginx和一层网关,网关有一个proxy_read_timeout 60s的配置。正常情况下10MB分片上传几秒钟就完成了,但有个别分片因为客户端所在网络比较差,TCP窗口协商加上TLS握手,耗时接近60秒,于是被网关掐断。

解决办法有两个方向:一是把网关和Nginx的proxy_read_timeout调大,比如120秒;二是把分片再切细一点(从10MB降到5MB),减少单片网络耗时。因为产品端用户网络环境确实较差,最终两个方案都做了,问题彻底消失。

这个案例说明一个容易被忽略的问题:分片上传方案不仅要考虑后端处理和合并逻辑,前端到后端之间的每一层代理的超时时间、请求头大小限制都要纳入设计参数。大附件上传是个全链路问题,不是单纯的后端接口问题。

还有一个类似的坑:有的负载均衡器对multipart/form-data请求会在转发时解析请求体,导致后端收到的分片变成内存对象,但没有文件头信息。一旦遇到“配置都对、上传就失败”的情况,优先检查链路中是否有额外组件做了请求体解析。

8. 回到项目视角:方案选型与落地顺序建议

聊了这么多细节,回到最早的“SpringMVC大附件上传有什么方案”这个问题上。我个人的建议是:10MB分片 + 后端分片目录存储 + 数据库状态跟踪 + 合并接口 + 前端并发控制,这套组合是通用性最强、实现成本可控、可维护性也好的方案。如果只想用现成框架,可以参考淘淘等开源项目,但理解原理依然重要,否则出了问题根本无从下手。

落地时建议按以下顺序推进:

  1. 先把基础分片上传跑通:前后端能分片传输、合并成功。
  2. 再加断点续传:后端提供分片查询接口,前端本地记录已传分片。
  3. 再加秒传:前端计算文件MD5,后端查重。
  4. 最后做运维加固:监控指标、清理任务、高可用配置。

每一步都不大,但叠加起来就是一个相当完整的企业级大文件上传服务。

关于分片大小、并发数、存储路径规划这些参数,不需要一开始就追求完美,上线前做一轮真实带宽、真实文件大小的压测,数据会告诉你该调哪。内部系统20并发、100MB文件起步压测,基本就能发现大部分隐藏问题。

我做了这么多年上传功能,最大的心得是:文件上传不是写完接口就结束的功能,它是一个涉及前端、后端、存储、网络、运维的完整闭环。先把原理吃透,再动手写代码,后面运维的日子会好过很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 4:09:16

AI自动测试实践:用视觉定位与OCR替代脆弱DOM定位

1. 内容整体设计与思路拆解1.1 为什么传统自动化测试越来越“难以为继”做测试这行&#xff0c;很多人都有一个相似的感受&#xff1a;最早学自动化时&#xff0c;觉得无非是录制回放、写脚本、跑断言&#xff0c;套路固定&#xff0c;工作一段时间之后就能驾驭。但随着产品迭代…

作者头像 李华
网站建设 2026/10/13 4:09:06

ThinkPHP5集成滑动验证:从登录防刷到中间件降级实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 4:08:28

新宇:人生重启的系统化操作指南,从目标拆解到习惯落地

这两年我陆陆续续接到了不少和“状态重启”“人生重新规划”相关的咨询&#xff0c;有相当一部分人聊到自己的处境时&#xff0c;都会提到同一个词——想换个活法&#xff0c;想把过去翻篇&#xff0c;想给自己一个新的开始。恰好我最近看到一个很有意思的标题“新宇”&#xf…

作者头像 李华
网站建设 2026/10/13 4:07:16

数字智能时代信号滤波:从经典到自适应再到深度学习融合

做信号处理这些年&#xff0c;我越来越觉得“滤波”这件事被低估了。提起滤波&#xff0c;很多人第一反应还是RC电路、低通高通、巴特沃斯那套经典理论&#xff0c;总觉得教科书里写得明明白白&#xff0c;没什么好聊的。但放到数字智能时代的语境下&#xff0c;滤波的定位已经…

作者头像 李华
网站建设 2026/10/13 4:07:13

MLP做全球海温预测:轻量级落地的务实路径

简介&#xff1a;本资源是一套基于多层感知机&#xff08;MLP&#xff09;的全球海表温度&#xff08;SST&#xff09;预测完整实践方案&#xff0c;面向高校地理信息、气象海洋及人工智能方向的学生&#xff0c;尤其适合开展课程大作业或入门级神经网络建模学习。包内含5个核心…

作者头像 李华
网站建设 2026/10/13 4:07:13

用规则引擎与大模型打造App Store审核Agent,让拒审可分析可预防

最近被拒得多了&#xff0c;很烦。不是一次两次&#xff0c;而是连续两三个提审版本接连吃灰&#xff0c;每次商店发来的通知都写得像一份加密电报——有条款编号&#xff0c;有截图标红&#xff0c;可就是说不清你那个功能到底哪里越了线。老实说&#xff0c;在动手之前我试过…

作者头像 李华