1. 项目概述:视频点播系统的核心价值

去年参与过一个在线教育平台的后端重构项目,客户原有的视频播放功能经常卡在500并发就崩溃。我们用SpringBoot重写的点播系统最终扛住了3000+并发,这让我意识到一个健壮的视频点播系统对业务有多重要。典型的视频点播系统需要解决三个核心问题:如何高效存储海量视频文件、如何实现流畅的渐进式播放、如何应对突发流量冲击。

SpringBoot的自动配置特性让我们能快速集成视频处理相关组件。比如用Spring Web MVC处理分片上传,HikariCP管理数据库连接池,Redis缓存热门视频的元数据。源码里最值得关注的是基于FFmpeg的视频转码模块和自定义的JWT鉴权流程,这两个部分直接决定了系统能否稳定运行。

2. 技术架构设计解析

2.1 分层架构设计

系统采用经典的四层架构:

  • 表现层:SpringMVC处理HTTP请求,Swagger生成API文档
  • 业务层:@Service封装核心逻辑,如视频转码、推荐算法
  • 持久层:MyBatis-Plus操作MySQL,Redis缓存热点数据
  • 存储层:MinIO对象存储视频文件,FFmpeg处理视频流

这种分层带来的最大好处是职责隔离。比如当我们需要把存储从MinIO迁移到阿里云OSS时,只需修改存储层的实现类,其他层代码完全不用动。在源码的 com.example.vod.config 包下可以看到各层的配置类。

2.2 关键技术选型对比

技术点 可选方案 最终选择 选择理由
文件存储 FastDFS/MinIO/阿里云OSS MinIO 开源可控,S3协议兼容,后期可无缝迁移到商业云存储
视频转码 FFmpeg/OpenCV FFmpeg 社区生态完善,支持H.265编码,GPU加速方案成熟
缓存 Redis/Memcached Redis 支持更丰富的数据结构,集群方案成熟
权限控制 Shiro/Spring Security Spring Security 与Spring生态无缝集成,OAuth2.0支持完善

实际开发中发现FFmpeg的硬件加速需要特别注意:在Linux环境下必须安装NVIDIA驱动和CUDA工具包,否则转码速度会慢5-8倍。这个坑我们在部署文档中有特别说明。

3. 核心功能实现细节

3.1 视频分片上传与断点续传

前端采用WebUploader实现分片,后端关键代码如下:

@PostMapping("/chunk-upload")
public ResponseEntity<String> uploadChunk(
    @RequestParam MultipartFile file,
    @RequestParam String chunkMd5,
    @RequestParam Integer chunkIndex) {
    
    // 校验分片MD5
    String serverMd5 = DigestUtils.md5Hex(file.getBytes());
    if(!chunkMd5.equals(serverMd5)){
        return ResponseEntity.badRequest().body("分片校验失败");
    }
    
    // 存储到临时目录
    String tempPath = "/tmp/chunks/" + chunkMd5;
    file.transferTo(new File(tempPath));
    
    // 记录分片信息到Redis
    redisTemplate.opsForSet().add("video:chunks:" + fileId, chunkIndex);
    return ResponseEntity.ok("分片上传成功");
}

合并分片时使用FFmpeg命令:

ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.mp4

其中filelist.txt包含所有分片的路径列表。实测上传1GB视频,分片大小为5MB时,断点续传成功率可达99.5%。

3.2 多码率自适应转码方案

在 VideoTranscodeService 中实现了以下转码逻辑:

  1. 原始视频转码为三种分辨率:
    • 1080P(码率2500kbps)
    • 720P(码率1200kbps)
    • 480P(码率600kbps)
  2. 生成HLS格式的m3u8播放列表
  3. 关键代码片段:
String cmd = String.format("ffmpeg -i %s -vf scale=w=-2:h=720 -c:v libx264 -b:v 1200k -hls_time 10 -hls_list_size 0 %s/720p.m3u8",
    originalPath, outputDir);
Process process = Runtime.getRuntime().exec(cmd);
int exitCode = process.waitFor();
if(exitCode != 0){
    throw new RuntimeException("转码失败,退出码:" + exitCode);
}

踩坑提醒:转码线程池大小需要根据CPU核心数设置。我们最初没限制并发数,导致服务器负载飙升。最终方案是配置固定大小为CPU核心数75%的线程池。

4. 性能优化实战记录

4.1 缓存策略设计

采用三级缓存架构:

  1. 本地缓存(Caffeine):缓存用户最近观看的5个视频信息
  2. Redis集群:缓存热门视频的元数据和m3u8文件
  3. CDN边缘节点:缓存视频分片文件

缓存更新策略:

@Cacheable(value = "videoInfo", key = "#videoId")
public VideoDTO getVideoInfo(String videoId) {
    // 数据库查询
    Video video = videoMapper.selectById(videoId);
    // 异步更新热门度计数器
    redisTemplate.opsForZSet().incrementScore("video:hotrank", videoId, 1);
    return convertToDTO(video);
}

4.2 数据库分库分表

当视频数量超过50万时,单表查询性能明显下降。我们按照视频ID的哈希值进行分片:

  • 主库:存储视频元数据,16个分片
  • 统计库:存储播放记录,按用户ID分片
  • 配置 application.yml :
shardingsphere:
  datasource:
    names: ds0,ds1,ds2
  sharding:
    tables:
      video:
        actual-data-nodes: ds$->{0..1}.video_$->{0..7}
        table-strategy:
          inline:
            sharding-column: id
            algorithm-expression: video_$->{id % 8}

5. 安全防护方案

5.1 防盗链实现

在 SecurityConfig 中配置防盗链过滤器:

http.addFilterBefore((request, response, chain) -> {
    String referer = request.getHeader("Referer");
    if(StringUtils.isNotBlank(referer) && !referer.contains("yourdomain.com")){
        response.sendError(403, "禁止外链访问");
        return;
    }
    chain.doFilter(request, response);
}, UsernamePasswordAuthenticationFilter.class);

5.2 视频URL时效控制

生成有时效性的播放地址:

public String generateSignedUrl(String videoId) {
    long expiry = System.currentTimeMillis() + 3600 * 1000; // 1小时有效
    String sign = DigestUtils.md5Hex(videoId + expiry + "secretKey");
    return String.format("/play?vid=%s&exp=%s&sign=%s", 
        videoId, expiry, sign);
}

6. 部署与监控方案

6.1 Docker Compose部署

docker-compose.yml 关键配置:

services:
  minio:
    image: minio/minio
    ports:
      - "9000:9000"
    volumes:
      - ./minio-data:/data
    command: server /data

  redis:
    image: redis:6
    ports:
      - "6379:6379"
    volumes:
      - ./redis-data:/data

  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - minio
      - redis

6.2 Prometheus监控配置

在 application.yml 中暴露指标:

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  metrics:
    tags:
      application: ${spring.application.name}

Grafana监控看板重点监测:

  • 视频转码队列积压数
  • 分片上传成功率
  • 播放请求P99延迟

7. 典型问题排查手册

7.1 播放卡顿问题排查流程

  1. 检查CDN命中率(低于90%需要预热)
  2. 查看转码后的视频关键帧间隔(建议2秒)
  3. 测试不同码率的m3u8文件下载速度
  4. 检查播放器缓冲区设置(建议初始缓冲2个分片)

7.2 常见错误代码

错误码 原因 解决方案
4001 分片MD5校验失败 检查前端分片大小设置
5002 转码进程异常退出 检查服务器FFmpeg版本和GPU驱动
4031 签名过期 重新生成播放链接
5023 存储空间不足 清理MinIO过期文件或扩容存储

这套系统在日活10万的在线教育平台稳定运行了8个月,期间最大的挑战是应对突发流量。我们通过自动扩容转码集群和动态调整HLS分片大小,成功扛住了万人同时在线观看的流量洪峰。源码中最值得参考的是 VideoStreamController 中的流量控制算法,它根据网络状况动态切换视频码率。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐