## 适用范围与版本
- 适用:内容站点/博客/资讯类服务的「热门文章」模块。
- 基线版本:Go 1.21+,Redis 6.2+(支持 ZADD/ZINCRBY 与精确过期),MySQL 8.0+ 作为主存储。
## 指标体系与权重设计(可验证)
- 采集指标:
- PV:页面浏览次数(抗爬虫后)。
- UV:独立访客数(基于 IP+UA 或登录态去重)。
- AvgDuration:平均停留时长(秒)。
- Interactions:点赞、评论、收藏等互动信号。
- AgeHours:文章发布后的小时数(用于时间衰减)。
- 推荐评分函数(可调整权重,默认值通过压测与离线回归得到):
- score = 0.35·log(PV+1) + 0.25·UV + 0.25·min(AvgDuration, 300)/60 + 0.10·Likes + 0.05·Comments − 0.02·AgeHours
- 参数选择依据:
- log(PV+1):缓解PV的头部效应,避免早期大量引流长期霸榜。
- UV 权重≥PV:真实覆盖更能体现热度质量。
- AvgDuration 上限 300s:防止极端长停留扭曲结果;按分钟归一化更稳健。
- 时间衰减 0.02/h:经验值;保证新文可上榜,旧文在无新增互动时逐步下滑。
## 数据采集与防作弊
- 采集:后端埋点上报 PV/UV/停留时长与互动事件,统一写入 Kafka/队列或直接记 Redis 计数。
- 防作弊:
- 端侧去抖:滚动/切页不重复上报停留时长;只在可见状态下计时(Page Visibility)。
- 服务端去重:`UV = COUNT(DISTINCT user_id || ip_hash || ua_hash)`;对异常高频 UA/IP 做速率限制。
- 机器人识别:User-Agent 黑名单与异常访问阈值(如 PV/s、UV/s)。
## Redis 排行结构与更新策略
- 使用 `ZSET` 存储热门分数:key `hot:articles`,member 为 `article_id`,score 为推荐分。
- 计数分桶:
- `cnt:pv:{article_id}:{yyyyMMddHH}`、`cnt:uv:{...}`、`cnt:dur:{...}`、`cnt:like:{...}`、`cnt:cmt:{...}`,按小时聚合,便于回放与重算。
- 更新策略:
- 实时:事件到达即更新对应计数并触发增量重算(节流至每 5–10s)。
- 批量:每分钟回放最近 24h 的小时桶,计算 AgeHours 与衰减并写 ZSET。
- 过期与归档:小时桶 7 天过期,定期将滚动窗口外数据归档到 MySQL(历史报表)。
## Go 端实现示例(可运行)
package rank
import (
"context"
"math"
"time"
"github.com/redis/go-redis/v9"
)
type Metrics struct {
PV int64
UV int64
AvgDuration float64 // 单位:秒
Likes int64
Comments int64
AgeHours float64
}
func Score(m Metrics) float64 {
pv := 0.35 * math.Log(float64(m.PV)+1)
uv := 0.25 * float64(m.UV)
dur := 0.25 * math.Min(m.AvgDuration, 300) / 60.0
likes := 0.10 * float64(m.Likes)
cmts := 0.05 * float64(m.Comments)
decay := 0.02 * m.AgeHours
return pv + uv + dur + likes + cmts - decay
}
type Store struct {
rdb *redis.Client
}
func NewStore(addr string) *Store {
return &Store{rdb: redis.NewClient(&redis.Options{Addr: addr})}
}
func (s *Store) Update(ctx context.Context, articleID string, m Metrics) error {
sc := Score(m)
return s.rdb.ZAdd(ctx, "hot:articles", redis.Z{Score: sc, Member: articleID}).Err()
}
func (s *Store) TopN(ctx context.Context, n int64) ([]string, error) {
return s.rdb.ZRevRange(ctx, "hot:articles", 0, n-1).Result()
}
// 计算 AgeHours 示例
func AgeHours(publishedAt time.Time, now time.Time) float64 {
return now.Sub(publishedAt).Hours()
}
## 定时任务与增量重算(伪代码)
// 每分钟回放最近 24h 小时桶,计算分数并写 ZSET
for _, aid := range activeArticleIDs() {
pv := sumHour("cnt:pv:", aid, 24)
uv := sumHour("cnt:uv:", aid, 24)
dur := sumHour("cnt:dur:", aid, 24) / max(float64(pv), 1) // 平均停留
likes := sumHour("cnt:like:", aid, 24)
cmts := sumHour("cnt:cmt:", aid, 24)
age := AgeHours(getPublishedAt(aid), time.Now())
_ = store.Update(ctx, aid, Metrics{PV: pv, UV: uv, AvgDuration: dur, Likes: likes, Comments: cmts, AgeHours: age})
}
## 验证方法与基线结果
- 本地压测:
- 生成 100 篇文章,模拟 10 分钟内不同 PV/UV/停留/互动分布,观察 TopN 稳定性与新文上榜速度。
- 期望:在 UV 提升且停留较长的文章更易上榜;早期高 PV 但低停留的文章得分会被抑制。
- 回归验证:
- 离线回放最近 7 天小时桶,比较新老权重的 TopN 差异与点击-转化提升(CTR/阅读完成率)。
- 边界检查:
- 极端高停留样本(>300s)不应导致异常霸榜(上限裁剪生效)。
- 无新增互动的旧文在 24–72h 内逐步下滑(衰减生效)。
## 与前端的衔接与展示
- API 返回:`article_id`、`title`、`score`、`summary`、`cover`、`published_at`。
- 展示策略:可在 TopN 中对相同作者/专题做去重,避免列表单一。
- 更新频率:建议 1–5 分钟;在高流量时段临时提高到 30–60 秒。
## 运维与监控
- 指标:ZSET 大小、TopN 命中率、事件消费延迟、重算耗时、Redis 命令耗时(`INFO latency`)。
- 预案:当 ZSET 热点过高,可分桶 `hot:articles:{category}` 按分类排行;或按标签维度做局部热门。
## 总结
- 以 UV/停留为核心、结合 PV 与互动信号,配合温和的时间衰减与小时分桶重算,可稳定产出高质量的热门榜单;参数需结合站点实际分布做回归与迭代。

发表评论 取消回复