10 KiB
短视频全局推荐环形队列——后端技术方案
1. 文档范围
本文仅覆盖《91porn迭代需求》第一项“短视频推荐逻辑修改”,以当前代码、测试H5和测试管理后台为准。
已确认的产品口径:
- 只推荐审核通过的短视频;
- VIP、金币、广告和特殊视频均可参与推荐,其他状态和内容类型不参与;
- 互动分为
点赞×1 + 收藏×2 + 评论×3 + 转发×5; - 使用全部累计真实互动,不使用运营假数据;
- 取消点赞、取消收藏、删除评论等行为不扣推荐累计分;
- 互动分和全局队列每日更新一次;
- 每批固定20条,严格按17条高分视频加3条新视频组织;
- 新视频按审核通过时间24小时计算,并从高分池排除;
- 新视频不足3条时用高分视频补齐;
- 用户偏移存Redis Hash;
- 每日新队列切换后所有用户从新队列起点重新开始;
- 队尾通过取模回到队首。
2. 现状核对
2.1 H5实际调用
测试H5短视频Swiper实际调用:
GET /api/app/recommend/vid/list?pageNumber={n}&pageSize={size}
当前响应:
{
"vInfos": [],
"totalPages": 10
}
H5还单独调用:
GET /api/app/recommend/vid/ad
GET /api/app/vid/user/count?vid={videoId}
广告插入和免费观看次数不并入本次推荐队列算法。
2.2 后端现状
主接口位于:
app/api/recommctrl/recommctrl.goapp/service/recommser/recommser.go
当前算法:
Redis Set(short-videos-all-ids-list)
-> SRANDMEMBER N
-> 查询视频详情
-> 返回
另有遗留接口:
GET /api/app/vid/module/short/all
该接口使用另一个Redis Set shortVideosRecoCache,并存在5分钟全局响应缓存。两条接口目前算法和Redis Key均不统一。
2.3 管理后台现状
后台已有“视频热度配置”:
GET /api/web/admin/vid/popularity/config
POST /api/web/admin/vid/popularity/config
该配置计算播放量、有效播放量、点赞量和审核时间衰减,是通用视频热度,不符合本次固定的互动分算法。因此:
- 本次推荐不能复用现有
VideoPopularityConfig; - 不修改现有热度配置页面;
- 一期不新增运营配置页面;
- 推荐权重按产品确认值作为后端常量;
- 灰度开关和任务时间使用服务配置。
3. 总体架构
真实互动事件
↓
视频推荐累计字段(只增不减)
↓
每日SKD任务计算recommendScore
↓
生成17+3全局有序队列
↓
完整写入Redis版本Key
↓
原子切换current版本
↓
推荐接口按用户Hash偏移环形读取
不在请求时实时计算分数,不为每个用户生成独立队列。
4. 数据模型
在视频模型增加:
RecommendLikeCount int64 `json:"-" bson:"recommendLikeCount"`
RecommendCollectCount int64 `json:"-" bson:"recommendCollectCount"`
RecommendCommentCount int64 `json:"-" bson:"recommendCommentCount"`
RecommendShareCount int64 `json:"-" bson:"recommendShareCount"`
RecommendScore int64 `json:"-" bson:"recommendScore"`
RecommendScoreAt time.Time `json:"-" bson:"recommendScoreAt"`
计算公式:
recommendScore =
recommendLikeCount
+ recommendCollectCount × 2
+ recommendCommentCount × 3
+ recommendShareCount × 5
4.1 为什么不能直接使用现有字段
现有 likeCount/collectCount/commentCount/shareCount 会在取消或删除时下降,而产品已确认取消行为不扣推荐分。
现有 fakeLikeCount/fakeCommentCount/fakeShareCount 包含运营假数据,禁止参与推荐分。
因此需要独立的单调递增推荐累计字段。
4.2 互动写入规则
仅在真实行为首次成功时递增:
- 点赞成功:
recommendLikeCount + 1 - 收藏成功:
recommendCollectCount + 1 - 评论发布成功:
recommendCommentCount + 1 - 转发首次有效记录:
recommendShareCount + 1
现有 POST /api/app/share/output 增加可选参数 videoID。新客户端分享短视频时必须传入,服务端仅在ID合法且视频当前为审核通过的短视频时累计;旧客户端不传保持原分享能力,但无法归属到具体视频,因此不累计推荐分享分。
以下行为不修改推荐累计字段:
- 取消点赞;
- 取消收藏;
- 删除评论;
- 运营修改假互动;
- 重复请求和重复事件。
每个互动入口必须先利用现有行为唯一性或幂等逻辑确认“首次成功”,再累计推荐字段。
5. 历史数据初始化
上线前执行一次幂等脚本:
recommendLikeCount = max(likeCount, 0)
recommendCollectCount = max(collectCount, 0)
recommendCommentCount = max(commentCount, 0)
recommendShareCount = max(shareCount, 0)
不读取任何 fake* 字段。
脚本增加初始化版本标记,重复执行不得覆盖已经由线上事件增长的新累计值。
6. 每日队列生成
任务放在 skd/job,使用现有Cron框架。默认按北京时间凌晨低峰运行,具体时间由部署配置确定。
6.1 候选条件
status = 审核通过
newsType = 短视频
未删除
VIP、金币、广告、特殊视频不作为排除条件。
6.2 新视频池
reviewAt >= generatedAt - 24h
reviewAt <= generatedAt
排序:
reviewAt desc, _id desc
进入新视频池的视频必须从高分池排除,当天队列中只使用一次。
6.3 高分池
排序:
recommendScore desc, reviewAt desc, _id desc
排除当日新视频池全部ID。
6.4 组装算法
每个推荐块:
high = 最多17条
new = 最多3条
缺少的新视频名额由high补齐
块内确定性洗牌
要求:
- 同一视频在当日全局队列只出现一次;
- 新视频消费完后,后续块全部由高分视频组成;
- 不复制视频凑足队列长度;
- 尾部不足20条时保留实际数量;
- 洗牌使用日期版本作为种子,便于问题复现。
7. Redis设计
recommend:short:current
recommend:short:queue:{version}
recommend:short:offset:{version}
recommend:short:meta:{version}
recommend:short:build-lock:{version}
示例:
recommend:short:current -> 20260724
recommend:short:queue:20260724 -> LIST(videoId...)
recommend:short:offset:20260724 -> HASH(uid => nextOffset)
recommend:short:meta:20260724 -> HASH(length, generatedAt, startOffset)
规则:
queue使用Redis List,保持顺序;offset使用一个Hash存放全部用户偏移;startOffset第一期固定为0;- 新版本先完整写入,再原子切换
current; - 新版本Key设置72小时TTL;
- 版本切换天然实现每日偏移重置,不扫描删除旧Hash;
- 构建锁避免多个SKD实例同时生成同一版本。
8. 环形读取
offset 表示下一次读取位置:
newOffset = (offset + scannedCount) % queueLength
使用Lua脚本原子完成:
- 读取当前版本;
- 读取队列长度;
HGET用户偏移;- 无偏移时使用0;
- 计算本次候选索引;
- 推进并保存新偏移;
- 返回版本、旧偏移和候选视频ID。
接口层查询Mongo并再次校验视频状态。发现失效视频时继续向后扫描补位,最多扫描一整圈。
约束:
- 同一响应内同一视频最多出现一次;
- 队列长度小于20时只返回一圈;
- Mongo返回结果必须按Redis ID顺序重新排列;
- 多设备和并发请求共享同一用户偏移。
9. App接口改造
9.1 H5主接口
保留现有地址:
GET /api/app/recommend/vid/list
修改 recommser.GetVidList,由随机Set切换到环形队列服务。
9.2 兼容接口
保留:
GET /api/app/vid/module/short/all
该接口删除5分钟全局响应缓存,并委托同一个环形队列服务读取,避免两个客户端获得不同推荐逻辑。
9.3 广告接口
保持不变:
GET /api/app/recommend/vid/ad
广告仍由H5在推荐内容之外按现有规则插入,不占17+3名额。
9.4 免费观看次数接口关联改动
现有接口:
GET /api/app/vid/user/count
传或不传 vid 的两个响应分支都增加:
{
"totalWatchCount": 3
}
该字段读取现有 appg.Conf.Base.TotalWatch,表示系统配置的免费观看总次数;现有 watchCount 继续表示用户剩余次数。不新增数据库字段,不修改扣减逻辑。此项已纳入接口设计,但代码尚未修改。
10. 响应兼容
H5主接口保留:
{
"vInfos": [],
"totalPages": 100
}
新增:
{
"hasNext": true,
"queueVersion": "20260724"
}
说明:
vInfos结构不变;totalPages继续返回ceil(queueLength / 20),仅兼容旧客户端;- 环形队列非空时
hasNext=true; - 前端不得用
pageNumber计算服务端offset; queueVersion仅用于日志排查。
11. 灰度、降级和监控
服务配置:
shortRecommendV2Enabled
shortRecommendCron
shortRecommendKeyTTLHours = 72
降级顺序:
- 当日队列不可用时使用上一有效版本;
- 没有任何版本或Redis异常时,回退现有
ShortVideosKey随机逻辑; - 降级必须记录日志和指标。
监控:
- 生成耗时和任务状态;
- 候选、高分、新视频和最终队列数量;
- 每块17+3比例;
- 重复率;
- 接口耗时和空结果率;
- 失效视频跳过数;
- Redis/Lua失败数;
- 版本切换时间;
- 降级次数。
12. 索引
建议Mongo索引:
status + newsType + reviewAt
status + newsType + recommendScore + reviewAt
根据生产数据量通过 explain 确认最终字段顺序。
13. 测试
必须覆盖:
- 正常17+3;
- 新视频0/1/2/3条;
- 新视频池耗尽;
- 高分池不足;
- 队列不足20条;
- 尾部跨界读取;
- 用户首次进入;
- 不同用户偏移隔离;
- 同用户并发;
- 每日版本重置;
- 队列中视频下架后的补位;
- 假数据不计分;
- 取消互动不扣分;
- 历史初始化幂等;
- SKD重复执行;
- 生成失败继续使用旧队列;
- H5主接口和兼容接口响应结构。
14. 发布步骤
- 发布新增字段和索引;
- 执行历史累计初始化;
- 发布互动累计逻辑;
- 发布SKD任务,影子生成队列但不切流;
- 核对数量、分数、17+3比例和重复率;
- 发布App接口兼容字段;
- 小流量开启
shortRecommendV2Enabled; - 全量切换;
- 保留旧随机Set作为短期降级路径。