--- sidebar_position: 6 title: Feed 算法 sidebar_label: Feed 算法 description: 广场热度排序公式、算法演化路径、Redis 计数策略和推荐迁移时机 --- # Feed 算法 ## 设计原则 - **初期不做个性化推荐**:冷启动时用户行为数据不足,协同过滤效果差于热度排序。 - **热度排序足够用到 10 万帖子**:时间衰减 + 多维互动权重,工程实现简单,效果可预期。 - **透明可调**:热度公式的权重系数写在配置表里,运营可以实时调整,不需要发版。 - **推荐算法是独立演化的模块**:等数据量够了再接入,接口协议从第一天就预留好。 --- ## 热度排序公式(初期) ``` hot_score = (V × w_v + L × w_l + C × w_c + S × w_s) / (T + decay_base) ^ decay_exp ``` | 变量 | 含义 | 默认权重 | | --- | --- | --- | | V | view_count 浏览量 | w_v = 0.1 | | L | like_count 点赞数 | w_l = 3.0 | | C | comment_count 评论数 | w_c = 5.0 | | S | collect_count 收藏数 | w_s = 4.0 | | T | 帖子年龄(小时),`NOW() - published_at` | - | | decay_base | 时间衰减基数 | 默认 2 | | decay_exp | 时间衰减指数 | 默认 1.5 | **示例**: 一篇帖子发布 3 小时后,获得 500 次浏览、20 个点赞、3 条评论、5 个收藏: ``` 分子 = 500×0.1 + 20×3 + 3×5 + 5×4 = 50 + 60 + 15 + 20 = 145 分母 = (3 + 2)^1.5 = 5^1.5 ≈ 11.18 hot_score ≈ 12.97 ``` 同样互动量、发布 24 小时后: ``` 分母 = (24 + 2)^1.5 = 26^1.5 ≈ 132.6 hot_score ≈ 1.09 ``` 时间衰减效果明显,保证新内容有机会上热门。 --- ## 权重配置表 ```sql CREATE TABLE plaza_algorithm_config ( key VARCHAR(100) NOT NULL, value DOUBLE NOT NULL, description VARCHAR(200) NOT NULL DEFAULT '', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO plaza_algorithm_config VALUES ('w_view', 0.1, '浏览量权重', NOW()), ('w_like', 3.0, '点赞权重', NOW()), ('w_comment', 5.0, '评论权重', NOW()), ('w_collect', 4.0, '收藏权重', NOW()), ('decay_base', 2.0, '时间衰减基数', NOW()), ('decay_exp', 1.5, '时间衰减指数', NOW()); ``` 后台定时任务读取配置表后计算,运营修改配置后下次计算周期生效。 --- ## 计算时机 ```text 触发条件 执行动作 ────────────────────────────── ────────────────────────────── 每 10 分钟定时任务 重新计算过去 48 小时内发布帖子的 hot_score 新帖发布到广场 立即计算初始 hot_score(基于 0 互动 + 年龄 0) 帖子互动量发生变化 不立即重算,等下一次定时任务 ``` **为什么不实时重算**:每次点赞都重算 hot_score 并回写 MySQL,在高并发下会造成锁竞争。Redis 实时累计计数,定时批量回写 + 重算是更稳定的方案。 --- ## Redis 计数策略 ```text # 每次浏览 INCR plaza:post:{id}:view_count # 点赞 INCR plaza:post:{id}:like_count # 取消点赞 DECR plaza:post:{id}:like_count # 防止负数 if GET plaza:post:{id}:like_count < 0: SET plaza:post:{id}:like_count 0 ``` **定时同步任务(每 5 分钟)**: ```text 1. 维护 Redis SET plaza:sync:post_ids(每次 INCR 时将 post_id SADD 入集合) 2. SMEMBERS plaza:sync:post_ids,批量 GET 计数 3. 批量 UPDATE plaza_posts … WHERE id IN (...) 4. SREM 已同步的 post_id ``` > 避免对 `plaza:post:*` 做全量 SCAN;帖子量大时 SCAN 会阻塞 Redis。Sprint 3 前帖子少时可临时 SCAN,上线前必须改为 SET 增量同步。 **Redis 数据持久化**:开启 AOF,每秒 fsync,允许最多 1 秒的计数损失。 --- ## Feed 缓存策略 ```text 广场首页热门 Feed(全部分类): 缓存 Key: plaza:feed:hot:all:page:1 TTL: 5 分钟 生成时机: 定时任务重算 hot_score 后刷新 分类热门 Feed: 缓存 Key: plaza:feed:hot:{category_slug}:page:1 TTL: 5 分钟 最新 Feed: 缓存 Key: plaza:feed:new:all:cursor:{cursor} TTL: 60 秒(最新列表变化快) 精选位(Sprint 5): 缓存 Key: plaza:featured:{position} TTL: 5 分钟 合并规则: 见 04-backend-api「精选与 Feed 合并规则」 新帖 status 变为 published 后: DEL plaza:feed:hot:* plaza:feed:new:*(下次请求重建) ``` --- ## Feed 接口降级策略 | 场景 | 降级行为 | | --- | --- | | Redis 不可用 | 直接查 MySQL `ORDER BY hot_score DESC`,性能下降但不中断 | | 热度未计算(新帖) | 按 `published_at DESC` 兜底 | | 分类无帖子 | 返回全局热门帖子,前端提示"该分类暂无内容" | --- ## 算法演化路径 ### 第一阶段:热度排序(现在) 纯公式,无需用户行为数据,工程简单。 ### 第二阶段:兴趣标签匹配(月活 1 万后) - 从用户的浏览历史和点赞行为中推断兴趣标签(与帖子的 `tags` 字段匹配)。 - 在热度排序基础上,对匹配用户兴趣的帖子 hot_score 乘以权重系数(1.2–1.5)。 - 不需要机器学习模型,纯 SQL 可实现。 ### 第三阶段:协同过滤(月活 10 万后) - 引入独立 Python 推荐服务。 - 基于用户行为矩阵(浏览、点赞、收藏)进行 item-based 协同过滤。 - 后端 `/api/plaza/v1/feed` 接口保持不变,内部调用推荐服务获取帖子 ID 列表。 - **接口协议从现在就预留**:响应中的 `posts` 数组顺序即为推荐顺序,客户端无需感知算法类型。 ### 第四阶段:实时流推荐(月活 100 万后) - Flink/Spark Streaming 实时处理行为流。 - 向量召回 + 精排模型。 - A/B 测试框架。 --- ## 防刷策略 初期使用简单规则,不上机器学习: - 同一 IP 对同一帖子,24 小时内只计 1 次有效浏览。 - 实现:Redis `SET plaza:view:{ip_hash}:{post_id} 1 EX 86400 NX`,SET 成功才 INCR 计数。 - 同一用户重复点赞(已有 UNIQUE 约束):数据库层保证幂等。 - 短时间内大量来自同一 IP 的点赞:限流中间件 + 运营人工核查。 - 评论刷量:同一用户对同一帖子每小时最多发 10 条评论。 --- ## 新帖冷启动 新帖发布后 hot_score = 0,会排在热门列表末尾,导致新帖永远没有曝光机会。 解决方案:「最新」Tab 独立于「热门」存在,新帖默认进入「最新」Feed,以 `published_at DESC` 排序。 「最新」是新内容的曝光入口,「热门」是经过验证的内容的沉淀展示。这两个 Tab 服务不同的用户动机。