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