做微视频管理系统这个选题,其实是在一次跟朋友聊天的过程中确定的。他那边接了一个校园短视频创作比赛的活儿,几十个参赛者上传视频,要让人家能在线预览、审核、管理,还要能按不同维度统计播放情况。一开始他想直接套一个现成的视频网站源码,结果发现那些系统要么太复杂,要么就是纯展示型,后台管理能力非常弱。后来我说不如自己搭一套,技术栈就选Java生态最顺手的Spring Boot加Vue,存储直接用云平台的对象存储,视频转码交给云端处理,前端播放走视音频流协议。就这么聊着聊着,一个相对完整的技术方案就浮现出来了。这篇文章就把这套系统的设计思路、核心模块实现和部署上线过程中踩过的坑,原原本本写出来。
1. 项目全貌:这套技术栈到底在解决什么问题
1.1 一个微视频平台,核心要管什么
微视频管理系统,本质上是解决“视频内容的采集、存储、处理、分发、审核、统计”这条链路的问题。很多人一听到“管理系统”,第一反应就是增删改查的CRUD,但视频这种非结构化数据和普通文本数据完全是两码事。你发布一篇文章,数据库里存个富文本就行了;但是一条视频,涉及原始文件、转码后的多个清晰度版本、封面图、字幕、各类元数据,这些异构数据怎么组织,决定了整个系统的上限。
我在设计这个系统的时候,把核心需求拆成了几个层面。第一层是内容接入,也就是用户上传视频的入口,要考虑文件大小限制、断点续传、云端直传还是服务端中转。第二层是内容处理,包括格式转码、分辨率抽帧、封面图生成、敏感信息审核。第三层是内容分发,也就是播放器如何拿到最优的播放地址,是否需要CDN加速,是否需要防盗链。第四层是内容管理,面向管理员的内容检索、上下架、分类、推荐位管理。第五层是数据统计,视频播放量、点赞量、完播率、用户增长趋势,这些数据要从一开始就埋点,否则后期再补非常痛苦。
1.2 Spring Boot + Vue,为什么这么选
选Spring Boot和Vue这个组合,在Java后端圈子里几乎是一个不用犹豫的选择。Spring Boot最大的价值在于它把Spring家族里那些复杂的配置全部自动化了,内嵌的Tomcat让你不用再单独部署一个Web容器,开发环境一个java -jar就能把服务跑起来。对于微视频管理系统这种业务场景,Spring Boot生态里的Spring Security做认证授权、Spring Data JPA或MyBatis做持久层、Spring Validation做参数校验,这些东西都是现成的,项目启动成本极低。
Vue这边,我倾向使用Vue 3加Vite的组合。Vite基于ES Module的开发服务器热更新速度比Webpack快一个数量级,这在开发阶段非常重要。我见过太多团队把时间浪费在等待前端编译上,一个组件改一下要等五六秒才能刷新,开发体验非常糟糕。Vue 3的Composition API配合setup语法糖写起来很舒服,组合式函数可以把视频列表的拉取、播放器的状态、数据统计逻辑都拆得清清楚楚,不像Options API那样所有逻辑都堆在data和方法里。
前后端分离带来的还有一个额外的好处,就是这套系统天然支持多端复用接口。后端的RESTful API设计好了,不仅Web端能用,以后如果要出小程序或者移动端App,直接调用同一套接口就行。我在设计接口规范的时候,统一使用了/api前缀加版本号的方式,响应体结构统一为code、message、data三个字段,这样前端处理异常逻辑就非常统一了。
1.3 云平台在这里扮演的角色
这个系统的标题里特别提到了“基于云平台”,这其实是整个系统的地基。云平台在这个项目里承担了三块核心职责。第一块是对象存储,视频原始文件和转码后的文件全部存放在对象存储服务中,而不是放在应用服务器的本地磁盘上。原因很简单,视频文件动辄几百MB甚至上GB,如果放在应用服务器磁盘上,不仅占用空间,而且应用服务器在扩容、迁移、重启的过程中,文件数据很容易丢失或损坏。对象存储的存储成本低、扩容方便、自带多副本冗余,天然适合存放这种大文件。
第二块是内容分发网络,也就是CDN。视频播放场景对延迟和带宽非常敏感,如果所有用户都直接访问对象存储的源站,一来源站的带宽成本高,二来跨地域的访问延迟会很严重。接入CDN之后,播放器请求的是CDN边缘节点的地址,节点会自动回源拉取内容并缓存,用户访问就近的边缘节点,首帧加载时间能缩短一半以上。
第三块是可选的视频处理服务。有些云平台提供了现成的视频处理接口,输入一个存储地址就能自动转码、截图、提取音频。我当时评估了一下成本和可控性,最终选择自己搭建基于FFmpeg的处理服务,因为云服务商的转码规格是固定的,没法完全按需定制。不过如果对视频处理的需求比较简单,直接用云服务商提供的转码功能也能省不少事,这块后面再细说。
2. 系统结构与数据模型设计
2.1 前端模块划分和路由规划
前端这块我按业务域拆分了几个核心模块:管理端和用户端。管理端是给运营人员用的,包含数据概览大屏、视频管理列表、用户管理、分类管理、审核任务管理几个主要页面。用户端是给内容创作者用的,功能相对简单,主要是上传视频、查看自己视频的数据表现、编辑视频信息。
路由层面我用了路由懒加载,也就是按需加载。这样首屏只加载当前页面需要的JavaScript代码,视频列表页、数据统计页这种比较复杂的页面,各自的代码会独立分包,加载速度会明显快一些。
角色权限这块是前后端配合实现的。前端根据用户角色动态生成可见的路由菜单,但核心的数据权限校验还是放在后端接口上。前端做路由拦截只是提升用户体验,不能作为安全的唯一手段,这个思路执行项目时一定要记住。
2.2 后端模块与接口设计
后端我拆分了以下几个主要模块:用户模块负责注册、登录、个人资料管理;视频模块负责视频信息的增删改查、上下架状态管理;上传模块负责获取上传凭证、上传进度回调、视频信息补充;处理模块负责转码任务的创建、状态查询、处理结果回调;统计模块负责播放量、点赞量、完播率等数据的采集和查询。
接口设计的核心原则是职责清晰。上传接口和视频管理接口分开,因为上传视频是一个异步的流程:客户端先获取一个上传凭证,然后直传对象存储,存储系统上传完成后再通过回调通知我们的后端服务,后端更新视频记录的状态为“已上传”,接着自动创建转码任务。整个链路里,如果所有逻辑都塞到一个接口里,同步等待转码完成,用户体验就会特别差,接口的响应时间也会非常长。
统一响应结构也花了点心思。我是用一个Result类包装所有接口的返回数据,比如Result.success(data)和Result.error(code, message)。前端在request拦截器里统一处理响应,如果code不是0,就弹出一个全局错误提示。这样后端不需要为每一个异常场景单独写接口说明,前端处理逻辑也很统一。
2.3 数据库表怎么设计才不返工
数据库是这种系统最容易返工的地方,前期设计得好不好,直接决定后期加功能的时候是改几张表还是要重写查询逻辑。视频信息表的核心字段我除了常规的标题、描述、分类ID、作者ID之外,还专门设计了几个容易被忽略的字段。
第一个是video_status,视频状态字段,我用了整数类型,0表示上传中,1表示已上传待处理,2表示处理中,3表示已上线,4表示已下线,5表示转码失败。为什么要用整数而不是字符串,因为整数做索引查询效率更高,而且状态流转的判断逻辑写起来更清晰。第二个是cover_url,封面图地址,很多人设计表的时候会忽略这个,等到前端展示视频列表需要封面图的时候才补,实际上视频上传之后就应该立刻让用户上传封面图,或者系统自动从视频中抽取一帧作为默认封面。
第三个是duration,视频时长字段,这个字段在列表页展示、数据统计中都非常常用。视频详情表则单独建了一张,用于存储不同清晰度版本的地址、码率、分辨率、文件大小。视频原始文件表和处理后的文件分开存,主要是因为一条视频可能有多个版本,一对多的关系用独立的表存更规范。
用户表我额外加了一个role字段,用tinyint区分普通用户和管理员。权限校验用Spring Security实现,登录成功之后发放JWT令牌,后续请求在拦截器里解析令牌并放行。要注意的是,JWT令牌的有效期不能设置太长,我一般设置24小时过期,配合Redis存储刷新令牌实现自动续期,这样既能保证用户体验,也能降低令牌泄露的风险。
3. 核心功能实现:从上传到播放的关键环节
3.1 云存储接入:视频上传这条链路怎么打通
视频上传是这个系统里第一个容易踩坑的环节。我采用的是“服务端签名,客户端直传”方案,也就是说,客户端不会把视频文件先传到我们的应用服务器,而是先请求后端接口获取一个临时上传凭证,凭证中包含存储空间的地址、访问密钥的签名信息和允许上传的路径。客户端拿到凭证后,直接把文件上传到对象存储服务。
这样设计有几个好处。最直接的好处是不占用应用服务器的带宽。一个视频文件上传可能要占用几十MB到几百MB的带宽,如果所有流量都经过应用服务器转发,应用服务器的带宽成本会非常高,而且Tomcat处理大文件上传的能力并不强,并发一高,服务很容易被打满。通过客户端直传,应用服务器只需要处理一个很小的凭证请求,大流量直接走对象存储的接口。
上传进度的展示也是一个关键细节。前端用分片上传的方式把大文件切成多个分片依次上传,然后通过计算已上传的分片占总分片的比例来展示进度条。这里有一个经验,就是分片大小设置为4MB到8MB比较合适,太小会导致请求数量过多,加大失败重试的复杂度,太大会导致单个请求超时。
3.2 转码与封面截图:用FFmpeg实现的几个细节
视频上传完成之后,紧接着就要处理转码了。用户上传的视频可能是MP4、AVI、MOV、MKV等各种格式,编码格式也是五花八门,但网页端播放器支持最好的格式基本就是H.264编码的MP4。所以我设定了一条转码目标:把所有的上传视频统一转成H.264编码、AAC音频编码的MP4格式,同时按分辨率生成多个清晰度版本,比如1080P、720P、480P,方便前端根据用户的带宽情况选择播放清晰度。
FFmpeg转码的核心命令大概是这样:
ffmpeg -i input.mp4 \ -c:v libx264 -preset fast -crf 23 -r 30 -g 60 \ -c:a aac -b:a 128k \ -vf scale=1920:1080 -movflags faststart \ output_1080p.mp4这里几个参数值得说一下。-crf 23是视频质量参数,取值范围0到51,数值越大画质越差、文件越小,23是一个比较均衡的推荐值。-preset fast是编码速度和压缩率的平衡选项,如果服务器性能好,可以改成slow,画质和压缩率会更好,但转码时间会明显增加。-movflags faststart非常关键,它会把MP4文件的元数据移动到文件头部,这样播放器在开始播放的时候不需要等到整个文件下载完才能获取到时长和索引信息,大大缩短首帧加载时间。不加这个参数,播放器可能需要加载相当大的数据量才能开始播放。
封面图抽取用的是FFmpeg的thumbnail滤镜,命令会从视频中采样出多张候选帧,然后根据画面质量打分选出一张最清晰的作为封面:
ffmpeg -i input.mp4 -vf thumbnail=100,scale=1280:720 -frames:v 1 cover.jpg这里thumbnail=100表示每100帧采样一次候选帧,然后在这些候选帧里评分选最佳。实际用下来,自动选出来的封面虽然大部分时候能看,但有时候也会选到黑屏或镜头切换的瞬间,所以正式上线时我给用户提供了手动上传封面图的功能,自动抽帧只是兜底方案。
转码任务的调度也是一个需要考虑的点。如果视频量不大,直接在一个线程里同步执行转码是可以的。但当并发上来之后,就必须用消息队列来做任务削峰。我用Redis的列表结构实现了一个简单的任务队列,后端服务收到上传完成回调之后,把转码任务ID推入Redis队列,然后有一个独立的消费者线程池从队列里拉取任务执行转码。这样即使短时间内突然提交了大量视频,也不会瞬间打爆服务器的CPU和内存资源。
3.3 视频播放:前端Video标签和流媒体方案的取舍
播放器部分,我用了Video.js作为基础播放器,配合HLS流媒体方案。所谓HLS,就是把一个完整的视频切分成一个个几秒的小分片文件,同时生成一个索引文件,播放器拿到索引文件之后,可以按需加载对应的分片。这样有几个好处,一个是支持自适应码率,网络带宽好的时候自动切换高清分片,带宽差的时候切换低清分片,避免一直卡顿。另一个是支持拖动播放,用户拖进度条的时候,播放器只需要加载那一个时间点对应的分片,不需要把整个视频文件拉下来。
HLS分片需要FFmpeg单独处理:
ffmpeg -i input.mp4 \ -c:v libx264 -c:a aac \ -hls_time 6 -hls_playlist_type vod \ -hls_segment_filename "output_%03d.ts" \ output.m3u8-hls_time 6表示每个分片6秒,-hls_playlist_type vod表示这是一个点播视频,生成的是固定不变的播放列表。转码完成之后,.m3u8索引文件和.ts分片文件都上传到对象存储,播放地址就是索引文件的URL。
这里有个经验教训:HLS分片生成完成之后,千万不要用简单的文件覆盖上传方式更新视频文件,因为旧分片和新分片的索引序号可能一模一样,但内容完全不同,CDN边缘节点缓存的分片会和新内容冲突。我当时的做法是给每一个转码任务生成一个唯一的目录名,比如video_2024001_1080p这样的路径,整个目录一次性上传,这样就不会存在缓存污染的问题。
4. 部署上线:云资源选型与容器化配置
4.1 服务器和存储该按什么标准买
部署这块,我建议初期不需要一步到位买昂贵的配置,但也不能太抠门。应用服务器用4核8G的配置起步比较合适,因为Java应用本身的内存占用就在1G到2G之间,加上Spring Boot框架的各种依赖和缓存,8G内存运行起来会比较从容。如果是纯粹的开发测试环境,2核4G也能带起来,但跑FFmpeg转码任务时会出现明显的卡顿。
存储这部分,视频文件一定要放在对象存储服务上,这是前面已经说过的。数据库用的是云数据库MySQL 8.0,为什么不用服务器自己装一个MySQL?核心原因是云数据库自带主从备份、自动故障切换这些能力,出了问题不用半夜爬起来手工恢复数据。Redis也用云服务商的实例,1G内存的基础版就够了,主要用来存验证码、JWT令牌刷新、视频处理任务队列这些数据。
CDN服务是视频平台必备的,开通之后在控制台添加加速域名,然后配置好源站为对象存储的地址。这里需要特别注意的是,如果视频资源是私有的或者需要付费观看的,就要开启URL鉴权功能,CDN节点在收到请求时会校验URL上的时间戳签名,签名过期的请求直接拒绝,这样就能防止视频资源被爬虫盗链。
4.2 Docker部署Spring Boot和Vue的配置经验
部署方案我用了Docker Compose来编排,把应用拆分成了几个独立的容器:后端Java服务、前端Nginx服务。后端服务的Dockerfile比较简单,核心就是把打好的JAR包放进JRE基础镜像里。但有几个坑要提前规避。
第一个是时区问题。默认的JRE基础镜像是UTC时区,但我们的系统是中国用户在用,数据库存储的时间如果用的是UTC时区,展示的时候就会差8个小时。Dockerfile里必须显式设置时区环境变量:
FROM eclipse-temurin:17-jre ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone COPY target/video-manager.jar /app/video-manager.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/video-manager.jar"]第二个是JVM参数配置。容器环境下,如果不在启动命令里配置最大堆内存,JVM默认会按照宿主机内存的四分之一来设置堆大小,这可能导致Java服务吃掉过多内存,和FFmpeg进程争抢资源。启动参数里我固定了-Xms512m -Xmx1024m,这样内存占用就在可控范围内。
前端Vue项目打包之后,构建出来的dist目录放进Nginx镜像里。Nginx的配置有几个重点,一是前端路由是history模式,需要配置try_files把未知路径都重定向到index.html;二是静态资源文件要开启gzip压缩和浏览器缓存,这样视频列表页里的JS、CSS、图片文件加载速度会快很多;三是接口请求要走反向代理转发到后端Java服务,这里要配置一个/server/路径的代理规则。
location /api/ { proxy_pass http://backend:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }后端Java镜像和前端Nginx镜像在同一个Docker网络里,前端容器通过服务名backend来访问后端容器,而不是通过IP地址,这样容器重启IP变化后,配置依然有效。
4.3 上线前必须检查的几个点
上线的第一件事是检查对象存储的访问权限。很多人开发的时候测试方便,对象存储的桶权限设置成了公共读,结果上线的时候忘记调整,视频资源被任何知道URL的人随意访问甚至下载,这是非常危险的安全漏洞。我把桶权限调整为私有读写,然后通过CDN和签名的临时URL来开放访问权限。
第二件事是数据库的连接池参数。Spring Boot默认的HikariCP配置在并发不高的时候没有问题,但视频管理系统在活动期间可能会出现短时间的高并发查询。我把maximum-pool-size设置成了20,minimum-idle设置成了5,connection-timeout设置成了30000毫秒。连接池的参数一定要在压力测试之后再确定,不要拍脑袋填一个值就上线。
第三件事是配置好日志输出。Java服务容器里如果日志只往控制台打印,容器一旦重启日志就全丢了。我把日志配置了FileAppender输出到宿主机挂载的日志目录,同时按天做轮转切割。这样出了问题,直接翻日志文件排查即可。
5. 实战中遇到的坑与排查记录
5.1 上传大视频总是超时
这个问题排查了挺久。现象是用户上传200MB以上的视频时,传到一半就报错中断,或者后端返回网关超时。一开始怀疑是Nginx的上传大小限制,检查之后发现client_max_body_size根本没有设置。后来发现是对象存储的分片上传并发数太高,家里的上行带宽撑不住这么多分片同时传输,出现大量的重传。解决方案是把分片并发数从5降到了2,同时把分片大小从4MB提到了8MB,实测之后上传成功率明显上升。
还有一个细节是DNS解析问题。客户端在请求获取上传凭证的时候用的是后端接口域名,直传对象存储的时候用的是存储服务的域名,这两个域名如果不在同一个DNS解析服务商下,某些地区的网络解析会比较慢,导致上传请求等待时间过长被判定为超时。这个问题的优化方式是在前端直接配置了对象存储的IP直连地址,绕开了DNS解析环节。
5.2 播放卡顿和首帧慢
视频播放卡顿,最常见的起因是CDN没有预热。新上传的视频转码完成之后,第一次播放时如果CDN节点没有缓存,播放器请求会直接回源到对象存储,而对象存储源站的带宽通常没有CDN节点那么充足,再加上源站到CDN节点的回源链路本身就有延迟,首帧加载耗时就会特别长。解决方案是上传流程里增加了一个预热接口,在视频上线时调用CDN服务商的预热API,让CDN提前把视频索引文件和关键分片拉取到边缘节点。
还有一个影响因素是HLS分片数量太多。如果转码时-hls_time设得太小,比如2秒一个分片,一部10分钟的视频就有300个分片文件,播放器逐个请求这些分片文件,每次请求都有网络往返耗时,累积起来就会感觉卡顿。把分片时长调整到6秒之后,分片数量减少到原来的三分之一,播放流畅度显著提升。
5.3 视频格式兼容性
用户上传的视频格式五花八门,有iPhone录制的MOV格式,有安卓手机常见的MKV格式,还有各种压缩软件导出的小众编码格式。一开始FFmpeg转码的时候,有一小部分视频会出现转码失败或者转码出来的视频没有声音,排查下来发现是音频流编码格式的问题。有的视频音频流是AC3编码,有的视频音频流是MP3编码但采样率比较奇葩,FFmpeg默认的AAC编码器处理不了这些输入流。
解决方案是在转码命令里增加音频流的过滤处理逻辑,先探测输入文件里的音视频流编码信息,如果音频流不是AAC格式,就在转码命令里加上-audio filter参数,强制把音频流重采样到44100Hz的AAC编码。还有一个附加处理是遇到没有音频流的纯视频文件,用-f anull加一个静音音轨,这样播放器在播放的时候就不会因为无法解码音频导致整体播放失败。
6. 这套系统还可以继续扩展的方向
做完整个微视频管理系统之后,如果还有时间和精力,有几个方向是值得继续扩展的。
一个是智能推荐。现在视频列表就是按照上传时间倒序排列的,用户刷到感兴趣的视频全靠运气,这个体验其实比较原始。可以在统计数据的基础上做一个简单的推荐引擎,根据用户的观看历史、点赞行为、停留时长来打标签,然后提取相似视频推荐给用户。不一定要上深度学习模型,基于标签的相似度计算就能取得不错的效果。
另一个是视频审核的自动化。目前内容审核靠人工在后台逐条查看,视频量小的时候还行,一旦内容量上来,人工根本看不过来。可以接入云端的内容审核接口,自动识别违规画面和敏感信息,机器审核不通过的转人工复核,形成一个半自动化的审核流水线。这块涉及到的技术主要是视频抽帧和图像识别,现在云平台都有现成的能力接口。
还有一个是移动端适配。目前系统在手机浏览器上也能用,但操作体验距离原生App还是有差距。如果后续有移动端的规划,前端可以直接使用uni-app或者Flutter把现有业务封装成小程序端和App端,后端接口基本不用动,只需要补充一些移动端特有的登录方式即可。
我做这套系统时最深的体会是,视频相关的系统,核心难点往往不在业务逻辑,而在文件和流的处理上。文件上传的链路优化、转码任务的调度、播放分片的选择,每一个环节都要带着对底层原理的理解去做,否则表面的功能做出来了,实际用起来还是会发现各种小毛病。希望这篇整理出的实现记录,能给正在折腾类似系统的朋友一些参考。