一、需求
假定我们现在有一个业务需求:展示一个热点榜单,展示五十条(下图是我在稀土掘金首页上展示的榜单)
从非功能性上来说,热榜功能通常是作为首页的一部分,或者至少是一个高频访问的页面,因此性能和可用性都要非常高。
问题关键点:
- 什么样的才算是热点?【产品经理会告诉你】
- 如何计算热点?【算法层面的,产品经理也会告诉你】
- 热点必然带来高并发,那么怎么保证性能?【高性能】
- 如果热点功能崩溃了,怎么样降低对整个系统的影响?【高可用】
二、一些热点模型
那对于第一个问题:什么样的才算是热点?
不同公司的计算方式都不太一样,但是都有一些基本规律。
- 综合考虑了用户的各种行为:例如观看数量、点赞、收藏等。
- 综合考虑时间的衰减特性:包括内容本身的发布时间,用户点赞、收藏的时间。
- 权重因子:这一类可以认为是网站有意识地控制某些内容是否是热点,它可能有好几个参数,也可能是只有 一个综合的参数。(取决于比如当前网络热点事件、国家政策等)
2.1 Hacknews模型
其中 P 是投票数(或者得票数),T 是发表以来的时间 (以小时为单位)。总体可以认为:得票数最重要,而后热度随着时间衰减。
2.2 Reddit模型
从根本上来说,Reddit 的模型考虑的核心因素就是赞成票、反对票,以及发帖时间。
2.3 微博模型
微博热搜榜是通过综合计算微博上的阅读量、讨论量、转发量等数据指标,以及话题或事件的参与人数、参与次数、互动量等数据指标,得出每个话题或事件的实时热度,并按照热度进行排序呈现的。只是给了宽泛的介绍,并没有公开具体的热榜计算算法。
❗本文将采用 Hacknews 的热点模型,P = 点赞数,G = 1.5,T = 已发表时间
三、异步定时计算热榜
3.1 实时计算的痛点
实时计算热榜的难点:全表扫描 + 全局排序
- 全表扫描:找出所有帖子的
点赞数和发表时间 - 计算每个帖子的
score并全局排序 - 找出
top50
3.2 异步定时计算
解决方案:每隔一段时间就计算一次热榜
在这个基础上,要进一步考虑:
1)怎么设计缓存,保证有极好的查询性能。【高性能】
2)怎么保证可用性,保证在任何情况下(即使 mysql 和 redis 全崩了)都能拿到热榜数据。【高可用】
如何实现一个定时器?
(1)利用 time.Ticker 实现(简单,只能实现固定时间间隔执行,不能定时定点执行)
func TestTicker(t *testing.T) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
for {
select {
case <-ctx.Done(): // 超时的情况
// select 中不要使用 break
return
case now := <-ticker.C: // note ticker.C是只读的 channel,每 1s 会产生一个 Time 类型的数据
log.Println("现在是:", now.UnixMilli())
}
}
}
(2)利用 cron 表达式及其开源库实现(既能实现固定时间间隔,也能实现定时定点执行)
func TestCronExpr(t *testing.T) {
expr := cron.New(cron.WithSeconds())
// 填写 cron 表达式
id, err := expr.AddFunc("@every 1s", func() {
t.Log("执行了")
})
assert.NoError(t, err)
t.Log("任务", id)
// 开始调度任务
expr.Start()
time.Sleep(time.Second * 10)
ctx := expr.Stop() // 意思是,你不要调度新任务执行了,你正在执行的继续执行
t.Log("发出来停止信号")
<-ctx.Done()
t.Log("彻底停下来了,没有任务在执行")
// 这边,彻底停下来了
}
注意:
- 我们使用
github.com/robfig/cron/v3这个库 - 参考这个文档来写
cron表达式:help.aliyun.com/zh/ecs/user… - 我们使用的 cron 开源仓库提供了一些便捷的写法,如下图
如何具体实现热榜算法?
(下文再见~)