侧边栏壁纸
博主头像
why

一个主要敲代码,经常怼文章,偶尔拍视频的成都人。

  • 累计撰写 197 篇文章
  • 累计创建 11 个标签
  • 累计收到 101 条评论

关于排行榜问题。我错了,打我可以,别打脸。

why
why
2021-03-05 / 2 评论 / 1 点赞 / 411 阅读 / 2,748 字
温馨提示:
关注公众号why技术,第一时间接收最新文章。

周一的时候不是发布了一篇关于排行榜的文章吗。

凉了呀,面试官叫why哥设计一个排行榜。

文如其名,真的是凉了。

写的时候可能被猪油蒙蔽了眼吧。

针对文章中的微信步数排行榜需求,经过读者提醒,我发现我给出的是一个非常 low 的方案。

我们出来混的,不,我们出来写文章的,有错就要认,挨打要立正。

知错就改正,所以现在,我们来重新看一下这部分,应该怎么去做。

不成熟的方案

再来说一下需求:

微信步数排行榜,我们每个人看到的都是自己微信好友的运动步数。
由于每个人的微信好友不一样,所以,每个人看到的排行榜都是各不一样的。

先说说我之前文章里面比较low的方案吧。

我是怎么一步步被带进去的呢?

因为我先是给出了一个基于 zset 的大家看到的都一样的排行榜,这部分是没有毛病的:

然后我说这个排行榜是有问题的,因为大家看到的都是一样的。

接着我说:

上图中红框框起来的这句话开始,我就跑偏了。

我的想法是通过操作有序集合的 key,在 key 里面设计一个用户的标识字段,为每一个用户维护一个步数排行榜,这样就能保证每个用户看到的排行榜都不一样了。

但是,我大意了啊。

如果一个用户的步数发生了变化,那么就需要对这个用户的所有好友的排行榜都进行一次更改。

假设我有 1w 个好友,按照我的方案,每个好友都维护了自己的排行榜,而他们的排行榜里面都有我。

所以当我的微信步数发生变化之后,这 1w 个好友的排行榜都需要更新一下。

这个成本是非常大的。

而且,成本大就算了,关键是成本大了,还不讨好。

为啥不讨好?

你想啊,我的分数一变化,就把 1w 个好友的排行榜都去更新一下。

这 1w 个好友会立刻去看排行榜吗?

绝大部分不会去看吧。这玩意,正常人一天也看不了几次吧?

费这么大的劲儿干的事,确实一个吃力不讨好的事。

这就是为每个用户维护一个排行榜的弊端。

捋一下场景

基于上面的这个不成熟的方案,我们怎么让他成熟起来呢?

我们从这两个角度出发去思考:

  • 1.每个用户的步数是否需要实时维护?
  • 2.排行榜是否需要实时生成?

来,首先第一个:每个用户的步数是否需要实时维护?

还是那句话:脱离业务场景的方案设计,都是耍流氓。

所以,我们带入一个真实的业务场景。

你想想,作为一个正常的人来说,每天为什么打开微信步数排行榜?

拿我自己举例,我刚刚跑完步、爬完山、遛完弯,我就单纯的想看看我跑了多少步。

这个场景应该是最多的吧?

所以,我并不是来看排行榜的,我只是想看看自己的数据。

这个时候,步数必须是实时的,或者近实时的。

不可能我跑步之前是 3000 步,我跑完之后,一看:

啥玩意啊,怎么还是 3000 步?

所以,每个用户的步数是否需要实时维护?

我觉得需要,非常需要。

再来看第二个:排行榜是否需要实时生成?

首先我定义一下这个实时生成的含义啊:

就是说我在运动的过程中,步数发生了变化,即使我没有去看排行榜,但是排行榜也在时刻发生变化。这个叫做实时。

这个我觉得是不需要的。

我们可以延迟生成,延迟到用户主动点击步数排行榜的时候,再去生成。

因为我每个好友的步数是近实时更新的,其实从我的角度来说,这个排行榜也几乎是实时的。

只是生成的实际从实时维护变成了请求时再生成。

而且,这个排行榜你还不能给我缓存起来。

你想啊,假设现在榜一的马同学走了 10000 步,而王同学走了 9800 步:

王同学只需要再多走 200 步,就可以位居榜一了。

王同学现在的需求是冲击榜一,结果走了 10 分钟后,进来看,怎么排行榜没有变化呢?

什么垃圾玩意?

正常看到的结果应该是两种。

第一种是,这 10 分钟走了 3000 步,如愿以偿,变成榜一:

第二种情况是,马同学这 10 分钟也没闲着,她也在运动,假设她运动了 4000 步:

此时,王同学还是榜二。

而且王同学无话可说,因为从排行榜的变化他知道了,马同学现在也在运动。

我举上面的这个例子是想说明:每当我请求一次的时候,你得基于最新的数据,给我生成最新的排行榜。

说一下方案

现在需求很明确了:步数近实时维护,排行榜请求的时候再去生成。

我们还是基于 Redis 去做。

但是我要声明一下的是:肯定有其他的工业级的解决方案,而且在这个场景下肯定需要一些其他的诸如 hbase 一类的存储来辅助实现,但是不在本文讨论范围之内,我们聚焦到 Redis 上。

步数,我们还是用 Hash 结构来存储。

key 是每个用户+每一天的维度,value 里面两个字段:

点赞数
步数

当有用户步数上传时,就去更新 value 里面的步数。

需要注意的是这个 Hash 结构是非常大的。

我们少说点,假设微信有一千万的用户(实际肯定远远不止这个数)在使用步数排行榜的功能。

这么多用户,每一天就是一千万条数据,我们肯定是要好几台机器存储的。

所以我们可以根据指定字段进行哈希计算,把这一千万的数据打散来存,也方便扩展。

当用户来查看排行榜的时候,要生成他的好友步数排行榜,我们需要知道这个用户有哪些好友。

这个用什么数据结构来存呢?

可以用图、或者直接 MySQL 设计表结构来存储好友关系,但是我们这里不搞复杂了。

如果仅限于从 Redis 的数据结构里面选的话,我觉得用 zset 比较合适。

为什么呢?

因为按照微信目前的规则,一个人最多 1w 个好友。极端情况下,一次性把 1w 个好友的数据都查出来,不好吧?

所以,我们得分页查询。而 zset 不就有支持分页查询的命令吗?

zrange/zrevrange key start end [withscores]

当有人查看排行榜的时候,先分页获取他的所有好友,再循环获取所有好友的步数,生成这个用户的排行榜。

这个时候的排行榜,我们还是利用 zset,就可以和之前文章中的设计接上了:

当把排行榜数据返回给前端之后,经过我们前面的分析,我们仅仅是利用 zset 的自动排序功能。

生成之后的排行榜并没有缓存的必要,所以这个排行榜就可以立即销毁。

其实这个地方,还是有个问题。

我们获取到了排行榜之后,还是得倒回去查一下每个用户的点赞数。

又是一次循环操作,感觉怪怪的。

这一瞬间我能想到一个骚一点的操作是把步数和点赞数凑在一起,直接把 score 变成一个小数。

比如 mx 今天的步数是 10158,点赞数是 25,那么她的分数就是 10158.25。

在步数不一样的情况下,点赞数并不会影响排行。

步数一样的情况下,谁点赞多,谁排前面:

只是这方案,扩展性略差啊。

一看时间快凌晨 2 点了,我不行了,大家集思广益吧,也不一定要局限于 Redis 中。

最后说一句(求关注)

最后,我要感谢这个读者的评论,指出了我之前方案的不足之处,引发了我的思考。

这不就是我经常说的嘛:相互学习,共同进步。

好了,看到了这里了,关注一下我的公众号[why技术]吧,文章写好后第一时间会先发布在公众号里面。

写文章很累的,需要一点正反馈。你的关注,就是强有力的正反馈!

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

抱拳了,铁子!

0

评论区