Files
huangguo_server/SHORT_VIDEO_RECOMMENDATION_BACKEND_TECH_DESIGN.md
T
rootandClaude Opus 5 8679200f41 Initial commit
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 13:57:10 +08:00

10 KiB
Raw Blame History

短视频全局推荐环形队列——后端技术方案

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.go
  • app/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脚本原子完成:

  1. 读取当前版本;
  2. 读取队列长度;
  3. HGET用户偏移;
  4. 无偏移时使用0
  5. 计算本次候选索引;
  6. 推进并保存新偏移;
  7. 返回版本、旧偏移和候选视频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

降级顺序:

  1. 当日队列不可用时使用上一有效版本;
  2. 没有任何版本或Redis异常时,回退现有 ShortVideosKey 随机逻辑;
  3. 降级必须记录日志和指标。

监控:

  • 生成耗时和任务状态;
  • 候选、高分、新视频和最终队列数量;
  • 每块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. 发布步骤

  1. 发布新增字段和索引;
  2. 执行历史累计初始化;
  3. 发布互动累计逻辑;
  4. 发布SKD任务,影子生成队列但不切流;
  5. 核对数量、分数、17+3比例和重复率;
  6. 发布App接口兼容字段;
  7. 小流量开启 shortRecommendV2Enabled
  8. 全量切换;
  9. 保留旧随机Set作为短期降级路径。