算法高级(25)-分布式TopN算法玄机

面试题:请问,某视频点播网站,希望实时展现热门视频,比如近8小时点击量最大的前100个视频。如果由你来开发这个功能,你怎么做?

一、思路分析

看到这样的一个问题,估计同学们会说,很简单呀,用Redis的SortedSet搞定啊,Score计数,Key是视频ID,不就OK了吗?

回答到这一步,只能说你回答的不错,下一位。too young too simple呀兄弟。

要听清楚题目,有8小时动态时间窗口,计数会过期的。还有,一个视频播放网站的量有这么小吗,一个Redis就搞定了?我告诉你,视频数据起码是百万级别了,用户几个亿,播放量你至少得估计个1000/s吧。

这么看下来,还觉得是一个Redis能解决得了的吗?

二、解决办法

数据量太大,肯定要使用分布式了。关于数据的接收、存储本章我们先不考虑,我们只分析如何统计某个时间段内的播放量排行。

我们需要一张表来记录每条视频的点击记录(包含时间,用户等信息),我们要统计的就是某个时间段的点击总次数。

如果只有一张表,而且数据也存储在数据库中,那这个sql就简单了,语句如下:当天播放量前100视频排行榜

SELECT
	( e.createTime ) AS HOUR,
	ROUND( sum( e.score ), 1 ) AS sum,
	id 
FROM
	t_vedio_play e 
WHERE
	to_days( e.playTime ) = to_days( now( ) )
order by score desc limit 100

视频量太大,一张表放不下所有的记录,就需要用到分治算法,先将表哈希拆分成1024张子表。每张表里有一个字段score,表示这个视频的点击数(其实就是1,我们记录的是每一个时间点的每一次点击行为,后面再对时间进行分段抽象)。

如果是多个子表,你得在每个子表上都进行一次TopN查询,然后聚合结果再做一次TopN查询。子表查询可以多线程并行,提高聚合效率。下面是伪代码:

candidates = []
for k in range(1024):
   # 每个表都取topn
   rows = select id, score from vedio_${k} order by score desc limit 100
   # 聚合结果
   candidates.extend(rows)
# 根据score倒排
candidates = sorted(candidates, key=lambda t: t[1], reverse=True)
# 再取topn
candidates[:100]

上面这个算法思想,实际上就是分而治之,我们通过把一个大问题拆分成多个小问题并行处理以提高效率。

三、进一步优化

8小时的滑动窗口,意味着新的数据源源不断的进来,旧的数据时时刻刻在淘汰。严格来说,精准的8小时滑动窗口要求每条数据要严格的过期,差了1秒都不行,到点了就立即被淘汰。

精准的代价是我们要为每条点击记录都设置过期时间,过期时间本身也是需要存储的,而且过期策略还需要定时扫描时间堆来确认哪些记录过期了。量大的时候这些都是不容小嘘的负担。

但是在业务上来讲,排行版没有必要做到如此的精准,偏差个几分钟这都不是事。

业务上的折中给服务的资源优化带来了机遇。我们对时间片进行了切分,一分钟一个槽来进行计数。下面是伪代码:

class HitSlot {
   long timestamp; # earlies timestamp
   map[int]int hits;  # post_id => hits
   void onHit(int postId, int hits) {
       this.hits[postId] += hits;
   }
}
class WindowSlots {
   HitSlot currentSlot;  # current active slots
   LinkedList historySlots;  # history unactive slots
   map[int]int topHits; # topn posts
   void onHit(int postId, int hits) {  
       # 因为上游有合并点击,所以有了hits参数
       long ts = System.currentTimeMillis();
       if(this.currentSlot == null) { # 创建第一个槽
           this.currentSlot == new HitSlot(ts);
       } elif(ts - this.currentSlot.timestamp > 60 * 1000) {  
                          # 创建下一个槽,一分钟一个槽
           this.historySlots.add(this.currentSlot);
           this.currentSlot = new HitSlot(ts);
       }
       this.currentSlot.onHit(postId, hits);
   }
   void onBeat() {  
       # 维护窗口,移除过期的槽,然后统计topn,30s~60s调用一次
       if(historySlots.isEmpty()) {
           return;
       }
       HitSlot slot = historySlots[0];
       long ts = System.currentTimeMillis();
       if(ts - slot.timestamp > 8 * 60 * 60 * 1000) {  
           # 过期了8小时,移掉第一个
           historySlots.remove(0);
           # 计算topn的帖子            
           topHits = topn(aggregateSlots(historySlots));  
       }
   }
}

上面的代码代表着每个分布式子节点的逻辑,它的目标就是定时维持一个8小时的统计窗口,并汇聚TopN的热门视频放在内存里。这个TopN的数据并不是特别实时,有一个大约1分钟的短暂的时间窗口。

四、统计中用到的Hash

按照视频的数据至少几百万,如果每个子节点都要对所有的播放量统计】,似乎也会占用不少内存,聚合和排序也会有不少计算量。最好的想法是每个子节点只负责一部分视频播放量的统计,这样可以明显节省计算资源。


我的微信公众号:架构真经(id:gentoo666),分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。每日更新哦!

参考资料:

  1. https://blog.csdn.net/bntX2jSQfEHy7/article/details/80276225

你可能感兴趣的:(算法高级)