news 2026/9/2 3:17:31

超长视频上传架构设计:分片上传、断点续传与削峰降本实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
超长视频上传架构设计:分片上传、断点续传与削峰降本实践

“2 小时视频,午高峰集中上传,用户网络还差,最后算下来单 GB 存储和转码成本低得离谱?”——如果你接手过视频类产品的后端,大概一眼就能看出这不是在吐槽,而是在描述一个真实的系统设计难题。

超长视频和普通短视频不一样。短视频可以一把梭,直接传完整文件,后端收到后立刻转码、切片、上 CDN。但长达一两小时的视频,原始文件动辄几个 GB,如果还按“整文件上传”的老思路设计,午高峰两小时就能把应用服务器的内存和带宽打满,更不用说移动网络环境下动不动断流、超时,最后前端报错、用户重传、后端堆积,全链路一起雪崩。

这篇文章不聊业务层面的“单价高低”,而是把这个问题拆成一套技术方案:超长视频为什么要分片上传,午高峰流量如何削峰,弱网环境怎样做断点续传与重试,以及最容易被忽视的“成本单价”问题——单位视频从上传到可播放,到底消耗多少存储、转码和流量,如何用工程手段把单价降下来。读完你可以得到一套完整的设计思路、可落地的关键代码,以及生产环境里的避坑清单。

1. 这篇文章真正要解决的问题

先说明白一个判断:超长视频上传最核心的技术矛盾,不是“文件大”,而是“体验 + 成本 + 稳定性”三者的平衡。

所谓超长视频,通常指时长在 1 小时以上,常见于网课回放、在线培训、监控录像、体育赛事录像、直播录制存档等场景。文件大小一般从 1GB 到 20GB 不等。这种视频文件如果直接用 FormData 提交到后端,会出现几个非常现实的问题:

  • 内存压力:应用服务器接收完整文件时,Node.js、Java 的 Multipart 解析器需要把文件写入临时目录或内存缓冲,大文件会拖垮容器。
  • 网络中断代价高:用户上传到 80% 断网,整文件重传,成功率极低。
  • 高峰期无法控制:午高峰两小时集中上传,带宽和存储写入会瞬间打满。
  • 成本不可见:从上传到转码、切片、CDN 分发,每一环节都有成本,不做架构设计则单价不可控。

本文要解决的,就是这几个问题。我会基于主流的“客户端分片 + 后端合并 + 异步转码 + 存储回调”方案,讲清楚架构选择、代码实现和成本控制。适合正在设计文件上传系统的后端工程师,也适合考虑自建视频处理平台的架构师。如果你只是做简单短视频上传,没必要用全套重型方案,但分片的思路同样有参考价值。

1.1 为什么不建议用“整文件上传”方案

先给出结论:整文件上传只适合小文件。把几十 MB 的文件整包上传,简单直接,几乎没有架构成本。但文件一旦进入 GB 级别,尤其是超长视频,整文件上传的缺陷会被急剧放大。

第一,HTTP 连接的有效期有限。移动网络下,一个连接可能几十秒就被运营商断开,整文件上传很难在一个连接内完成。第二,服务端需要配置超大的请求体限制,这等于把整个后端暴露在内存溢出风险中。第三,失败重启的成本是 O(文件大小)。用户下一次断点,就要重新传整个大文件,产品体验上几乎不可接受。

所以更稳妥的方案,是把大文件切成若干分片(Chunk),每个分片单独上传,服务端按顺序合并。这样单个请求的事务体量小、失败代价低、并发可控,还可以利用分片做秒传和断点续传。

2. 超长视频上传的核心概念与适用场景

2.1 分片上传

分片上传是把一个大文件切分成多个二进制块,逐块上传到服务端临时存储区,等所有分片上传完成后,由服务端按顺序合并成完整文件。

分片大小的选择需要权衡。分片太小,请求数太多,网络开销大;分片太大,单请求失败概率变高。一般经验值是 4MB 到 10MB。对超长视频,我习惯用 8MB 或 10MB 分片,这取决于用户的平均网络状况。从实际工程看,4MB 更适合弱网,8MB 更均衡。

2.2 秒传

秒传不是真正的“秒传”,而是通过文件内容唯一标识(通常取文件 MD5,或取前若干字节 + 大小组合的指纹)到服务端查询。如果服务端已经存在相同内容的文件,就跳过上传,直接返回成功。

超长视频场景下,秒传的价值很高。比如同一场直播的回放视频,多个用户重复上传源文件;或者运维把同一份监控录像反复归档。通过内容指纹去重,可以节省大量带宽和存储成本。

2.3 断点续传

断点续传依赖分片级状态记录。客户端每次上传前,先向服务端查询该文件已上传了哪些分片,然后只上传缺失的分片。这样即使中途断网、应用被杀,重试时不需要从头开始。

实现上需要三个要素:全局文件标识(fileId)、分片编号(chunkIndex)、已上传分片列表。Redis 是常见的选择,因为需要高频读写分片状态,并且天然支持过期时间。

2.4 异步转码

上传完成后,不能立刻认为“大功告成”。超长视频需要转码成适合浏览器播放的 HLS 流,生成多码率版本,再切片成 .ts 片段,最后上传到对象存储/CDN。这个过程耗时可能超过原视频时长,必须放在异步任务队列中执行,而不能在 HTTP 请求里同步等待。

概念解决什么问题常见实现
分片上传大文件传输不稳定前端 File.slice + 后端分片合并
秒传重复文件浪费带宽文件 MD5 + 存储侧指纹去重
断点续传网络中断后整包重传Redis 记录分片偏移
异步转码超长视频处理耗时消息队列 + 转码 Worker

3. 整体架构设计

超长视频上传系统的架构,可以用一条主线描述:客户端切片 → 预签名直传对象存储 → 服务端确认合并 → 异步转码 → 产物回调 → 可播放。

之所以不把分片先传到应用服务器再转存,是为了避免应用服务器成为带宽瓶颈。比较推荐的做法是用对象存储的预签名 URL,让客户端分片直传 MinIO/OSS/S3,应用服务器只负责校验状态和触发合并。

模块划分大致如下:

  • 网关层:负责限流、鉴权、路由。
  • 上传接入服务:负责创建上传任务、查询分片状态、合并分片。
  • 临时存储层:存放已上传的分片,可用 Redis 记录分片元数据,用对象存储存放分片实体。
  • 转码任务队列:上传完成后投递任务,实现削峰填谷。
  • 转码 Worker:消费任务,调用 FFmpeg 转码、切片、上传产物。
  • 元数据服务:维护视频状态、分片信息、播放地址。

这个架构的特点是:应用服务器不直接传输文件实体,只处理“谁在传、传到哪、传完没有”的控制流。数据流走对象存储,控制流走应用服务,两者解耦,高峰并发时也不容易打爆后端。

4. 环境准备与依赖选型

为了让示例具备可操作性,这里用一套通用技术栈说明。版本不写死,以你实际项目为准,核心思路不变。

后端推荐:

  • Java 17 或 JDK 11+,Spring Boot 3 或 2.7。
  • MinIO 作为对象存储,兼容 S3 API,本地部署方便。
  • Redis 6+ 保存分片元数据。
  • RabbitMQ 或 Kafka 做异步任务队列。
  • FFmpeg 5.x 做转码切片。

如果你熟悉 Node.js,也可以用 Node 实现同样的逻辑,但本文代码以 Java + Spring Boot 为例,思路同样适用于 Go、Python。

客户端推荐:

  • Web 端用 Vue/React,通过 axios 或者 XMLHttpRequest 实现分片并发。
  • 微信小程序端用 wx.uploadFile,每次上传一个分片。
  • 移动端 App 用 OkHttp 或原生 HttpClient。

我们以 Web 端为例,因为逻辑最清晰。

4.1 启动本地依赖服务

本地开发时,需要先启动 Redis 和 MinIO。MinIO 可以使用 Docker 快速启动:

docker run -d \ --name minio \ -p 9000:9000 \ -p 9001:9001 \ -e "MINIO_ROOT_USER=minioadmin" \ -e "MINIO_ROOT_PASSWORD=minioadmin" \ minio/minio server /data --console-address ":9001"

启动后,打开http://localhost:9001,用minioadmin/minioadmin登录,创建video-upload桶。权限建议默认私有,后续通过预签名 URL 临时授权。

Redis 的启动方式取决于本机安装方式,通常可直接执行:

redis-server

4.2 创建 Spring Boot 项目

可以直接使用 Spring Initializr 创建项目,引入以下依赖:

spring-boot-starter-web spring-boot-starter-data-redis minio amqp(如果使用 RabbitMQ)

另外需要手动引入 MinIO SDK,Maven 坐标如下:

<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.5.7</version> </dependency>

版本以 Maven 仓库最新稳定版为准,不建议使用过旧版本。

5. 完整示例代码实现

下面采用一个精简但完整的分片上传示例,路径和类名保持简单,方便直接对照。核心流程是:

  1. 前端调用POST /upload/init创建上传任务,返回uploadId
  2. 前端把文件切片,逐个调用POST /upload/chunk上传分片,服务端存到临时目录。
  3. 上传完成后调用POST /upload/complete,服务端合并分片成完整文件,并投递转码任务。

5.1 后端:初始化上传任务

文件路径:src/main/java/com/example/upload/controller/UploadController.java

package com.example.upload.controller; import com.example.upload.service.UploadService; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/upload") public class UploadController { private final UploadService uploadService; public UploadController(UploadService uploadService) { this.uploadService = uploadService; } @PostMapping("/init") public Map<String, Object> init(@RequestBody InitRequest req) { return uploadService.initUpload(req.getFileName(), req.getFileSize(), req.getChunkSize()); } } class InitRequest { private String fileName; private long fileSize; private long chunkSize; public String getFileName() { return fileName; } public void setFileName(String fileName) { this.fileName = fileName; } public long getFileSize() { return fileSize; } public void setFileSize(long fileSize) { this.fileSize = fileSize; } public long getChunkSize() { return chunkSize; } public void setChunkSize(long chunkSize) { this.chunkSize = chunkSize; } }

对应 Service 实现:

package com.example.upload.service; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.Map; import java.util.UUID; import java.util.concurrent.TimeUnit; @Service public class UploadService { private final StringRedisTemplate redisTemplate; public UploadService(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } public Map<String, Object> initUpload(String fileName, long fileSize, long chunkSize) { String uploadId = UUID.randomUUID().toString().replace("-", ""); long chunkCount = (fileSize + chunkSize - 1) / chunkSize; // 保存上传元数据,24 小时后如果没有完成上传则自动清理 redisTemplate.opsForHash().put(uploadId, "fileName", fileName); redisTemplate.opsForHash().put(uploadId, "fileSize", String.valueOf(fileSize)); redisTemplate.opsForHash().put(uploadId, "chunkCount", String.valueOf(chunkCount)); redisTemplate.opsForHash().put(uploadId, "chunkSize", String.valueOf(chunkSize)); redisTemplate.expire(uploadId, 24, TimeUnit.HOURS); return Map.of( "uploadId", uploadId, "chunkCount", chunkCount, "chunkSize", chunkSize ); } }

初始化阶段的核心作用是拿到一个全局唯一的uploadId,并把文件的基本信息记录下来。后续所有分片上传请求都要携带这个uploadId,服务端才能知道分片归属于哪个文件。

5.2 后端:上传分片与查询已上传状态

文件路径:src/main/java/com/example/upload/service/UploadService.java(追加方法)

public Map<String, Object> uploadChunk(String uploadId, int chunkIndex, MultipartFile file) throws IOException { String finalDir = "/tmp/video-chunks/" + uploadId; File dir = new File(finalDir); if (!dir.exists()) { dir.mkdirs(); } File target = new File(dir, String.valueOf(chunkIndex)); file.transferTo(target); // 标记该分片已上传 redisTemplate.opsForHash().put(uploadId, "chunk:" + chunkIndex, "1"); return Map.of("success", true, "chunkIndex", chunkIndex); }

Controller 中需要调整接收方式:

@PostMapping("/chunk") public Map<String, Object> uploadChunk( @RequestParam("uploadId") String uploadId, @RequestParam("chunkIndex") int chunkIndex, @RequestParam("file") MultipartFile file) throws IOException { return uploadService.uploadChunk(uploadId, chunkIndex, file); }

注意,生产环境不建议把分片写到本地磁盘,这里仅为演示。生产更推荐直接写入 MinIO 的临时桶:一个分片就是一个 object,分片名为{uploadId}/{chunkIndex}。这样即使用户上传失败,分片数据落在对象存储,也不会占用后端服务器磁盘。

查询已上传分片,可以帮助前端实现断点续传:

public Map<String, Object> getUploadedChunks(String uploadId) { Map<Object, Object> entries = redisTemplate.opsForHash().entries(uploadId); List<Integer> uploaded = new ArrayList<>(); for (Map.Entry<Object, Object> entry : entries.entrySet()) { String key = entry.getKey().toString(); if (key.startsWith("chunk:")) { uploaded.add(Integer.parseInt(key.substring("chunk:".length()))); } } return Map.of("uploadedChunks", uploaded); }

5.3 后端:合并分片

文件路径:src/main/java/com/example/upload/service/UploadService.java(追加方法)

public Map<String, Object> completeUpload(String uploadId) throws IOException { String finalDir = "/tmp/video-chunks/" + uploadId; File dir = new File(finalDir); File[] chunkFiles = dir.listFiles(); if (chunkFiles == null || chunkFiles.length == 0) { throw new IllegalStateException("分片不存在"); } // 按分片序号排序 Arrays.sort(chunkFiles, Comparator.comparingInt(f -> Integer.parseInt(f.getName()))); String mergedFileName = "/tmp/video-final/" + uploadId + ".mp4"; File mergedFile = new File(mergedFileName); if (!mergedFile.getParentFile().exists()) { mergedFile.getParentFile().mkdirs(); } try (FileOutputStream fos = new FileOutputStream(mergedFile)) { for (File chunk : chunkFiles) { Files.copy(chunk.toPath(), fos); } } // 合并完成后清理分片临时文件,投递转码任务 deleteRecursively(dir); redisTemplate.delete(uploadId); // 这里可以发送 MQ 消息,触发异步转码 // rabbitTemplate.convertAndSend("video.transcode", uploadId); return Map.of("success", true, "filePath", mergedFileName); } private void deleteRecursively(File dir) { if (dir == null || !dir.exists()) { return; } File[] files = dir.listFiles(); if (files != null) { for (File f : files) { if (f.isDirectory()) { deleteRecursively(f); } else { f.delete(); } } } dir.delete(); }

合并的逻辑比较简单:读取所有分片,按序号顺序写入同一个输出流。但这里隐藏着一个问题:必须保证所有分片都上传完整,否则合并出来的文件是损坏的。所以在 complete 之前,需要校验实际分片数量是否等于初始化时的chunkCount

校验逻辑:

public Map<String, Object> completeUpload(String uploadId) throws IOException { Map<Object, Object> entries = redisTemplate.opsForHash().entries(uploadId); long expectCount = Long.parseLong(entries.getOrDefault("chunkCount", "0").toString()); String finalDir = "/tmp/video-chunks/" + uploadId; File dir = new File(finalDir); File[] chunkFiles = dir.listFiles(); if (chunkFiles == null || chunkFiles.length != expectCount) { throw new IllegalStateException("分片不完整,期望 " + expectCount + ",实际 " + (chunkFiles == null ? 0 : chunkFiles.length)); } // 后续合并逻辑相同 }

这个校验必须做,否则弱网场景下前端漏传一个分片,后端合并后得到的视频时长会缩短或直接损坏。

5.4 前端:分片上传核心代码

文件路径:web/upload.js

const CHUNK_SIZE = 8 * 1024 * 1024; // 8MB async function uploadLargeFile(file) { const chunkCount = Math.ceil(file.size / CHUNK_SIZE); // 1. 初始化上传任务 const initResp = await fetch('/upload/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName: file.name, fileSize: file.size, chunkSize: CHUNK_SIZE }) }); const { uploadId, chunkCount: serverChunkCount } = await initResp.json(); // 2. 查询已上传分片,实现断点续传 const uploadedSet = new Set(); try { const statusResp = await fetch(`/upload/status?uploadId=${uploadId}`); const { uploadedChunks } = await statusResp.json(); uploadedChunks.forEach(index => uploadedSet.add(index)); } catch (e) { // 首次上传无状态,忽略 } // 3. 并发上传分片,控制并发数避免把带宽占满 const CONCURRENCY = 3; let cursor = 0; async function worker() { while (cursor < chunkCount) { const index = cursor++; if (uploadedSet.has(index)) { continue; } const start = index * CHUNK_SIZE; const end = Math.min(file.size, start + CHUNK_SIZE); const blob = file.slice(start, end); const formData = new FormData(); formData.append('uploadId', uploadId); formData.append('chunkIndex', index); formData.append('file', blob, file.name); let retry = 0; while (retry < 3) { try { await fetch('/upload/chunk', { method: 'POST', body: formData }); uploadedSet.add(index); break; } catch (e) { retry++; if (retry >= 3) { console.error(`分片 ${index} 上传失败`); throw e; } await new Promise(resolve => setTimeout(resolve, 1000 * retry)); } } } } await Promise.all(Array.from({ length: CONCURRENCY }, () => worker())); // 4. 通知后端合并 const completeResp = await fetch('/upload/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uploadId }) }); return completeResp.json(); }

并发数需要控制,不是越多越好。午高峰时段,大量用户同时上传,如果每个客户端都开 10 个并发分片,网关和对象存储的请求数会暴涨。建议并发数在 3 到 5 之间,同时在网关层设置单用户上传并发限制。

6. 弱网与恶劣场景的容错策略

“恶劣天气”在视频上传场景里,转化为技术问题就是弱网、高延迟、高丢包、连接频繁中断。这种情况下,分片上传能解决“断点续传”,但还需要额外策略降低失败率。

6.1 分片级重试与指数退避

客户端对每个分片独立重试,重试间隔采用指数退避,而不是立即重试。比如第一次失败等 1 秒,第二次等 2 秒,第三次等 4 秒。避免弱网时大量重试请求叠加,反而堵死信道。

6.2 感知网络质量并动态调整并发

可以通过navigator.connectionAPI(如果浏览器支持)读取effectiveType,如果是slow-2g2g,把并发数降为 1,减小分片大小至 2MB。分片更小,单次请求失败的成本更低。这种做法对移动端用户尤其有效。

6.3 服务端幂等性

同一个分片重复上传,服务端必须能覆盖或者忽略。上面的uploadChunk实现天然满足幂等:文件直接覆盖写入对应序号,Redis 状态置为已上传。不要让“重复上传”变成报错,否则弱网下的自动重试会反过来造成分片状态混乱。

6.4 完成态兜底扫描

当用户弱网导致最后一个分片迟迟传不过来,前端可能会反复请求 complete,但服务端因为分片不完整而拒绝合并。更好的做法是:前端定期查询已上传分片,只补传缺失的分片,避免盲目重试所有分片。代码里已经有uploadedSet跳过逻辑,这正是断点续传的价值。

7. 高峰两小时的削峰设计

午高峰两个小时内,所有用户集中上传,系统面临的是双高压力:一方面是带宽压力,另一方面是分片合并与转码的 CPU 压力。这两者有不同的削峰策略。

7.1 带宽层削峰

对象存储的预签名直传是首选。客户端拿到预签名 URL 后,直接把分片 PUT 到对象存储,应用服务器完全不经过文件数据。这样可以避免后端带宽成为瓶颈,也是云上视频系统的基本架构。

MinIO 生成预签名 URL 的示例:

import io.minio.GetPresignedObjectUrlArgs; import io.minio.MinioClient; import io.minio.http.Method; MinioClient client = MinioClient.builder() .endpoint("http://localhost:9000") .credentials("minioadmin", "minioadmin") .build(); String url = client.getPresignedObjectUrl( GetPresignedObjectUrlArgs.builder() .method(Method.PUT) .bucket("video-upload") .object(uploadId + "/" + chunkIndex) .expiry(3600) .build());

前端拿到 URL 后,直接把分片 PUT 到该 URL,而不经过你的应用服务器。

7.2 合并层削峰

如果把合并操作放在 complete 请求里同步执行,午高峰每个 complete 请求都会占用一个 Tomcat 线程去合并一个大文件,服务很容易被打死。建议把合并操作改造成异步任务:complete 接口只负责校验分片数量,然后发一条 MQ 消息,由合并 Worker 消费。这样 complete 接口的响应时间会非常短,系统吞吐量显著提高。

7.3 转码层削峰

转码是 CPU 密集型操作。一小时视频转码可能需要几分钟到几十分钟,取决于服务器配置。如果在高峰时段同时触发大量转码,CPU 会满负荷。解决办法:

  • 消息队列按 RPS 限流消费。
  • 转码 Worker 数量固定,不因为任务多而无限扩容。
  • 设置队列积压监控,一旦积压超过阈值,自动扩容 Worker。

整体上,高峰两小时的流量可以被队列缓冲成“全天持续消化”,这就是削峰填谷的核心思想。

8. 运行结果与验证方式

以本地 Demo 为例,完成前端上传后,你可以在内存桶或临时目录看到合并后的文件。用工具直接验证文件的完整性,是最简单也最可靠的确认方式。

8.1 校验合并后视频完整性

ffprobe /tmp/video-final/{uploadId}.mp4

如果输出正常显示 Duration、Stream 信息,说明合并成功。如果ffprobe直接报错,大概率是分片缺失或排序问题。

8.2 观察上传状态

对超长视频,前端可以打印每个分片的上传耗时和重试次数:

console.log(`分片 ${index} 上传成功,耗时 ${Date.now() - startTime}ms`);

观察弱网环境下重试次数是否在可控范围,如果某个分片重试超过 5 次仍然失败,就应该中止会话并提示用户切换网络。

8.3 验证断点续传

可以手动模拟断网再恢复。上传到一半,断开网络 10 秒,恢复后重新执行uploadLargeFile。如果uploadedSet生效,前端不会重复上传已成功的分片,日志中会看到大量“跳过分片”的记录。

8.4 判断系统是否打满

用 JVM 监控或者top命令观察应用服务器的 CPU 和内存。如果应用服务器在峰值期间 CPU 使用率正常,而对象存储或带宽成为瓶颈,说明架构已经达到了削峰目的;反之,如果应用服务器的线程数打满、内存飙高,说明仍有大量数据流经过后端,需要检查是否走了直传路径。

9. 常见问题与排查思路

问题现象可能原因排查方式解决方案
合并后的视频损坏或时间短分片缺失或分片顺序错误对比 Redis 中的 chunkCount 与磁盘分片数complete 前严格校验分片数量,并按序号排序合并
上传到一半失败,前端重试后上传量巨大没有实现断点续传或查询已上传分片逻辑查看客户端请求中是否存在 status 接口调用前端增加 status 查询,跳过已上传分片
午高峰应用服务器内存飙升文件数据经过应用服务器中转查看网络流量和 Tomcat 线程状态改用 MinIO/OSS 预签名直传
分片上传成功但 complete 超时合并操作耗时太长,同步阻塞在请求链路中查看 complete 接口的耗时合并操作改为异步消费 MQ 消息
转码任务积压越来越严重转码 Worker 数量不足或消费速率低查看队列积压数量和 Worker CPU限制转码并发、提升 Worker 数量、增加机器
Redis 中 uploadId 丢失上传超时超过 24 小时被过期清理查看 Redis TTL 配置延长过期时间或增加续期逻辑
弱网环境上传成功率低并发过高或分片过大查看前端日志中的失败分片号降低并发数、减小分片大小、增加重试退避

排查时遵循一个原则:先看控制流,再看数据流。用户上传失败,先确认初始化接口是否返回 uploadId,再确认分片是否到达对象存储,然后确认 complete 是否触发,最后看转码任务是否被消费。按这个链路逐段定位,能快速缩小问题范围。

10. 高峰降本与工程最佳实践

回到题目里的“恶劣天气单价这么低”,在视频上传系统里真正需要追问的是:单位视频的存储和处理成本,能不能通过架构手段降下来。以下几种手段在超长视频场景下收益很高。

10.1 内容去重存储

同一个视频文件可能被多个用户重复上传。通过文件 MD5 作为唯一标识,在上传前先查重,如果已经存在,则直接引用已有的存储对象,不再分配新的存储空间。超长视频体积大,去重省下的存储成本非常可观。

具体的做法是:初始化接口中携带文件 MD5,服务端判断该 MD5 是否已存在。如果存在,直接返回一个“秒传完成”标记,前端可以跳过所有分片上传。

10.2 分级存储策略

视频平台不是所有内容都有同样的热度。直播录像、监控回放这类超长视频,播放频率可能很低,完全可以放在低频存储层,单价更低。上传完成后先放在标准存储,根据播放统计,超过一定时间没有播放量就自动沉降到低频存储或归档存储。

这个策略对“恶劣天气单价低”的问题是最直接的回应:不是所有视频都值得用最贵的存储和 CDN。

10.3 转码产物只保留必要码率

超长视频的转码成本是 O(视频时长),如果每个视频都生成 1080p、720p、480p、360p 四个码率,存储和转码成本都会很高。更合理的做法是:根据视频用途决定码率档位。网课回放一般 720p 足够,监控录像 480p 即可;只有高质量视频才需要 1080p。

10.4 建设观测体系

成本问题不只看总量,要看单量。建议在元数据中记录每个视频的存储大小、转码耗时、转码消耗 CPU 时间、CDN 流量,从业务侧按天聚合。只有把“单价”可视化,才能知道降本策略是否生效。

10.5 安全与权限提醒

涉及对象存储和文件上传,有一点必须强调:预签名 URL 是有时效性的。不要生成有效期过长的 URL,建议控制在 30 分钟到 1 小时内。同时,上传接口必须做身份鉴权和配额限制,防止恶意用户用超长视频上传接口拖垮存储。

生产环境做任何配置变更(如修改存储桶策略、调整 Redis 清理策略),都要先在测试环境验证,做好备份和回滚方案,并遵循最小权限原则。

11. 总结与后续学习方向

超长视频上传不是一个“把文件传上去就行”的简单需求。它涉及分片协议设计、弱网容错、高峰削峰、异步转码、成本控制等多个环节。本文重点讲清楚了三条主线:如何用分片上传和断点续传解决超长视频传输不稳定的问题;如何用预签名直传、异步合并、消息队列解决午高峰集中上传的系统压力;如何用内容去重、分级存储、码率策略控制单位视频的处理成本。

建议你动手从最小 Demo 开始,先跑通分片上传 + 合并的逻辑,再加入 Redis 断点续传,最后接入对象存储直传和异步转码队列。每一步都验证成功后,再逐步补充秒传、鉴权、限流、监控等生产能力。

下一步可以继续深入的方向包括:基于内容指纹的跨节点去重、分片级校验码设计、基于 MQ 的任务可靠投递与重试、以及视频质量与转码成本的自动权衡策略。如果在生产环境落地,这些内容每一样都值得单独展开。

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

分子动力学模拟C代码编译与改造实战:以Rapaport为例

简介&#xff1a;《分子动力学模拟的艺术》是一本由D.C. Rapaport撰写、剑桥大学出版社出版的经典分子模拟教程&#xff0c;这份资源正是该书配套的C语言程序代码&#xff0c;适合分子动力学方向的研究生、科研人员以及想要深入理解模拟实现细节的自学者&#xff0c;可将书中的…

作者头像 李华
网站建设 2026/9/2 3:16:28

YOLOv8+PySide6实现PCB缺陷检测系统开发实战

在PCB制造与电子组装行业里&#xff0c;外观质检一直是产能瓶颈。过去依赖人工目检&#xff0c;效率低、漏检率高&#xff0c;而且招工困难。传统机器视觉方案需要针对每类缺陷单独写规则&#xff0c;遇到光照变化、板面脏污、器件遮挡时鲁棒性很差。深度学习目标检测模型的出现…

作者头像 李华
网站建设 2026/9/2 3:16:00

单片机Proteus仿真入门到进阶:300例源码的拆解与避坑指南

简介&#xff1a;这是一套面向单片机初学者和进阶者的 Proteus 仿真实例合集&#xff0c;涵盖 C51 编程、LCD1602 液晶显示、矩阵键盘、数码管、中断、PWM、ADC 及电机控制等常见嵌入式开发知识点。300 个实例均配有可运行的源代码和注释&#xff0c;适合在虚拟仿真环境中边学边…

作者头像 李华
网站建设 2026/9/2 3:15:13

浏览器端跑LLM?WebGPU本地推理实战与验证指南

如果你的电脑已经装了 Python、配好了 CUDA、下载了好几个 GB 的模型文件&#xff0c;才发现代码在服务器上跑得很顺&#xff0c;换个环境就崩了——那你会不会想过&#xff1a;能不能直接在浏览器里把模型跑起来&#xff1f;这不是异想天开。近几年 WebGPU、WebAssembly、WebN…

作者头像 李华
网站建设 2026/9/2 3:14:16

STM32H743基础例程实战:从时钟配置到OV2640图像采集

简介&#xff1a;面向STM32H743高性能MCU开发者的基础例程代码合集&#xff0c;覆盖GPIO输入中断、看门狗、定时器、PWM输出与捕获、LCD显示、SRAM管理7类关键外设&#xff0c;示例基于ARM Cortex-M7内核&#xff0c;帮助嵌入式开发者在官方库或HAL库基础上快速理解寄存器配置与…

作者头像 李华
网站建设 2026/9/2 3:13:51

基于YOLOv8的交通标志识别系统实战:从模型训练到Jetson Nano部署

简介&#xff1a;这是一套基于C与OpenCV实现的交通标志检测与识别完整项目&#xff0c;面向中高级视觉开发者和课程设计&#xff0c;配套可运行工程源码与数据集&#xff0c;可直接编译使用。项目自带图形化界面&#xff0c;左侧支持导入图片或视频并实时显示画面&#xff0c;右…

作者头像 李华