系统设计实战 10:设计 TikTok(短视频推荐平台)
🚀 系统设计实战 10:设计 TikTok(短视频推荐平台)
摘要:TikTok(抖音)是全球最大的短视频平台,核心在于极速播放和成瘾性推荐。本文深入剖析视频上传转码流水线、CDN 智能预加载、多级流量池推荐机制、实时用户行为反馈闭环,并提供完整的 Java 实现。
🎯 场景引入
你打开抖音,每个视频都精准命中你的兴趣。这个短视频推荐系统是如何做到"比你还了解你"的?
核心挑战:
- 实时推荐:用户每滑一个视频就产生一个信号,如何秒级更新推荐?
- 内容理解:如何通过视觉、音频、文本多模态理解视频内容?
- 探索与利用:推荐太精准用户会审美疲劳,如何平衡?
一、问题背景
1.1 核心挑战
TikTok 的体验特点: 1. 秒开:划到下一个视频,几乎 0 缓冲 2. 无限流:不需要关注任何人,系统永远有推荐内容 3. 流量大:视频比图片大 100 倍,带宽成本是天文数字 4. 推荐准:用户越刷越停不下来 核心矛盾: 视频体积大 vs 秒开体验 海量内容 vs 精准推荐 新内容冷启动 vs 公平曝光1.2 核心需求
| 需求 | 说明 |
|---|---|
| 视频上传 | 支持多格式上传、自动转码、内容审核 |
| 秒开播放 | 首帧时间 < 200ms |
| 智能推荐 | 千人千面,基于行为实时调整 |
| 互动功能 | 点赞、评论、转发、合拍 |
| 创作者工具 | 滤镜、音乐、特效 |
| 内容安全 | AI 审核 + 人工审核 |
1.3 容量估算
| 指标 | 数值 |
|---|---|
| DAU | 10 亿 |
| 每日视频上传量 | 500 万条 |
| 每日视频播放量 | 1000 亿次 |
| 平均视频时长 | 30 秒 |
| 平均视频大小 | 5MB(转码后) |
| 每日新增存储 | 500万 × 5MB × 4分辨率 = 100TB |
| CDN 带宽峰值 | ~50 Tbps |
| 推荐请求 QPS | ~200 万 |
二、整体架构
┌─────────────────────────────────────────────────────────────┐ │ 客户端 (App) │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ 视频播放 │ │ 视频上传 │ │ 互动操作 │ │ 预加载管理器 │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ └───────┼──────────────┼─────────────┼────────────┼───────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌───────────────────────────────────────────────────────────┐ │ API Gateway │ └───────┬──────────────┬─────────────┬────────────┬─────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌──────────────┐ ┌───────────┐ ┌──────────┐ ┌──────────────┐ │ Feed Service │ │Upload Svc │ │Action Svc│ │ Recommend │ │ (视频流) │ │ (上传) │ │ (互动) │ │ Service │ └──────┬───────┘ └─────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ 核心服务层 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │ │ │Transcode │ │ Content │ │ Feature │ │ Traffic │ │ │ │ Pipeline │ │ Review │ │ Store │ │ Pool Mgr │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────┬──────┘ │ └──────┼─────────────┼────────────┼──────────────┼────────┘ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ 存储层 │ │ ┌──────┐ ┌──────┐ ┌────────┐ ┌───────┐ ┌────────────┐ │ │ │ OSS │ │MySQL │ │ Redis │ │ HBase │ │ CDN 节点 │ │ │ │(视频)│ │(元数据)│ │(计数器)│ │(行为) │ │ (全球分发) │ │ │ └──────┘ └──────┘ └────────┘ └───────┘ └────────────┘ │ └─────────────────────────────────────────────────────────┘三、数据模型设计
3.1 核心实体
// 时间复杂度:O(N),空间复杂度:O(1)
/** * 短视频 */@Entity@Table(name="videos")publicclassVideo{@IdprivateLongvideoId;privateLongauthorId;privateStringtitle;privateStringdescription;privateStringmusicId;// 背景音乐 IDprivateStringtags;// 标签,逗号分隔privateIntegerduration;// 视频时长(秒)privateIntegerstatus;// 0=转码中 1=审核中 2=已发布 3=已下架privateStringoriginalUrl;// 原始视频 URLprivateStringtranscodedUrls;// JSON: {"360p":"url","720p":"url","1080p":"url"}privateStringcoverUrl;// 封面图 URLprivateIntegertrafficPoolLevel;// 流量池等级 1-5privateLongcreateTime;}/** * 用户观看行为 */@Entity@Table(name="watch_events")publicclassWatchEvent{@IdprivateLongeventId;privateLonguserId;privateLongvideoId;privateIntegerwatchDuration;// 实际观看时长(ms)privateIntegervideoDuration;// 视频总时长(ms)privateBooleancompleted;// 是否完播privateIntegerreplayCount;// 复播次数privateIntegeractionBitmap;// 位图:bit0=点赞 bit1=评论 bit2=分享 bit3=收藏privateLongcreateTime;}/** * 流量池配置 */publicclassTrafficPool{privateIntegerlevel;// 流量池等级privateIntegerexposureCount;// 曝光量privateDoublecompletionRateThreshold;// 完播率阈值privateDoublelikeRateThreshold;// 点赞率阈值privateDoublecommentRateThreshold;// 评论率阈值}四、核心模块实现
4.1 视频上传与转码流水线
/** * 视频上传服务 * * 流程:分片上传 → 合并 → 转码 → 审核 → 发布 */@ServicepublicclassVideoUploadService{@AutowiredprivateObjectStorageClientossClient;@AutowiredprivateTranscodeJobProducertranscodeProducer;@AutowiredprivateVideoRepositoryvideoRepository;/** * 初始化分片上传 */publicUploadInitResponseinitUpload(LongauthorId,StringfileName,longfileSize){StringuploadId=UUID.randomUUID().toString();intchunkSize=2*1024*1024;// 2MB 每片inttotalChunks=(int)Math.ceil((double)fileSize/chunkSize);// 生成每个分片的预签名上传 URLList<String>uploadUrls=newArrayList<>();for(inti=0;i<totalChunks;i++){Stringkey=String.format("uploads/%s/chunk_%d",uploadId,i);StringpresignedUrl=ossClient.generatePresignedUploadUrl(key,30,TimeUnit.MINUTES);uploadUrls.add(presignedUrl);}returnnewUploadInitResponse(uploadId,totalChunks,uploadUrls);}/** * 完成上传,触发转码 */publicVideocompleteUpload(LongauthorId,StringuploadId,VideoMetadatametadata){// 1. 合并分片StringoriginalKey="videos/original/"+uploadId+".mp4";ossClient.mergeChunks(uploadId,originalKey);// 2. 创建视频记录Videovideo=newVideo();video.setVideoId(SnowflakeIdGenerator.nextId());video.setAuthorId(authorId);video.setTitle(metadata.getTitle());video.setDescription(metadata.getDescription());video.setMusicId(metadata.getMusicId());video.setTags(String.join(",",metadata.getTags()));video.setOriginalUrl(originalKey);video.setStatus(0);// 转码中video.setTrafficPoolLevel(1);// 初始流量池video.setCreateTime(System.currentTimeMillis());videoRepository.save(video);// 3. 提交转码任务TranscodeJobjob=newTranscodeJob();job.setVideoId(video.getVideoId());job.setSourceKey(originalKey);job.setTargetResolutions(Arrays.asList("360p","480p","720p","1080p"))