前言
排行榜几乎已经成为互联网应用中的必备模块,特别是游戏领域,它是对某一相关同类事物的客观实力的反映,带有相互之间的比较性质,带有竞争意义。
对于平台来说,可以带来一定的权威性,提高平台影响力。
对于商家来说,可以带来更多的曝光,并对比自身的不足加以改进。
对于用户来说,可以为行动决策做参考,降低相关风险成本。
那么排行榜如何实现?我将结合自身的经验提供一些简单设计思路。
基于mysql
SELECT ORDER BY
对于小型的低频业务系统,mysql已经可以支撑所有的排序需求,类似班级排名,成绩排名都比较好实现。
直接SELECT ORDER BY
即可。
SELECT name, score, @rank := @rank + 1 AS rank
FROM test, ( SELECT @rank := 0 )
ORDER BY score DESC
其中@rank := 0
是为了在生成查询结果表的时候生成一组递增的序列号
mysql中
:=
和=
的区别
=
只有在set和update时才是和:=一样,用于赋值,其它都情况用作等于判断,1表示真,0表示假。
:=
用于变量赋值
如果想要计算排第几名就找出他分数高的有几个,再加上自身个数就好了。
SELECT count(1)+1 as '排名'
FROM test
WHERE score>(SELECT score FROM test WHERE name='李四')
需要注意的是,该sql语句会进行全表的扫描,所以对于大表来说就不适用了。
加索引
一般来说,在数据比较多的时候,排行榜中我们一般不会进行全表排名,否则成本开销会很大,所以会按前100、500、1000这样来排名。此时我们只要在需排序字段加个索引,然后limit即可。
SELECT name, score, @rank := @rank + 1 AS rank
FROM test, ( SELECT @rank := 0 )
ORDER BY score DESC
LIMIT 100
这样mysql就会优先走索引,避免了全表扫描。
加缓存
对于mysql来说,索引的增删查改也是需要维护的,所以如果对于需要频繁修改的排序字段,并且是非实时的排序需求(例如按小时、按天、按月等),我们可以考虑在写入前加缓存,避免频繁操作数据库,影响其性能。
例如:一分钟可能需要增减score字段值50次,那么我们可以由缓存先接手请求,等一分钟后,再统一写入数据库,那么这一分钟数据库操作的次数就少了50次,另外读取排行的时候也可以加缓存,效果显著。
当然缓存期越长,提升越多,但是也要考虑到缓存失效导致数据丢失等情况,来保证数据的一致性。
借助redis
对于非实时的排行来说,mysql+缓存是可以支撑业务,但是如果需要实时的排行,mysql就力不从心了,此时我们需要基于高性能的内存来进行操作。
没错,redis大兄弟又登场了,借助redis的高性能、原子性、以及丰富的数据结构,能够帮助我们快速实现许多传统数据库很难完成的事。
这次我们需要借助到的是redis的有序集合(sorted set)
有序集合是通过包含跳表和哈希表的双端口数据结构实现的,因此,每次添加元素时,Redis的复杂度都是O(log(N))。当我们要求排序的元素时,Redis根本不需要做任何工作,它已经全部排序了。
这里我们主要用到以下几个命令
ZADD key score1 member1 [score2 member2]
向有序集合添加一个或多个成员,或者更新已存在成员的分数
ZINCRBY key increment member
有序集合中对指定成员的分数加上增量 increment
ZRANK key member
返回有序集合中指定成员的索引
ZREVRANK key member
返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
ZRANGE key start stop [WITHSCORES]
通过索引区间返回有序集合指定区间内的成员
ZREMRANGEBYRANK key start stop
移除有序集合中给定的排名区间的所有成员
实时排行榜
由于用户的score数据还是需要记录保存的,所以上述的mysql方案依旧需要执行,只不过实时部分由redis接手。
以前100名实时数据为例。
1、若集合未初始化,读取mysql中排名前100,写入redis有序集合中。
2、当数据发生变动
- 判断score是否低于最后一名,是则直接忽略。
- 写入redis有序集合中。
3、实时排名直接从redis读
4、定期移除排名外的元素。
实现原理
redis有序集合实现的核心数据结构是【跳表】,大概长这样
对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高,是 O(n)。
如果像图中那样,对链表建立一级“索引”,查找起来是不是就会更快一些呢?每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引或索引层。
当链表的长度 n 比较大时,比如 1000、10000 的时候,在构建索引之后,查找效率的提升就会非常明显。
这种链表加多级索引的结构,就是跳表。
熟悉JAVA的同学肯定知道,hashmap是基于数组+链表的方式,当链表大小超过阀值之后,会变为红黑树来提高效率。
为什么 Redis 要用跳表来实现有序集合,而不是红黑树?
Redis 中的有序集合支持的核心操作主要有下面这几个:
插入一个数据;
删除一个数据;
查找一个数据;
按照区间查找数据(比如查找值在 [100, 356] 之间的数据);
迭代输出有序序列。
其中,插入、删除、查找以及迭代输出有序序列这几个操作,红黑树也可以完成,时间复杂度跟跳表是一样的。但是,按照区间来查找数据这个操作,红黑树的效率没有跳表高。
对于按照区间查找数据这个操作,跳表可以做到 O(logn) 的时间复杂度定位区间的起点,然后在原始链表中顺序往后遍历就可以了。这样做非常高效。
Redis 之所以用跳表来实现有序集合,还有其他原因,比如,跳表更容易代码实现。虽然跳表的实现也不简单,但比起红黑树来说还是好懂、好写多了,而简单就意味着可读性好,不容易出错。还有,跳表更加灵活,它可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
不过,跳表也不能完全替代红黑树。因为红黑树比跳表的出现要早一些,很多编程语言中的 Map 类型都是通过红黑树来实现的。我们做业务开发的时候,直接拿来用就可以了,不用费劲自己去实现一个红黑树,但是跳表并没有一个现成的实现,所以在开发中,如果你想使用跳表,必须要自己实现。
超过全国**%的用户
最早看到这个提示是在腾讯管家和360卫士上的开机助手提示,有点意思,那如何简单实现呢?
1、可以肯定的是我们不会去进行实时检索,而是采用离线计算。
2、我们可以基于正态分布,定期去计算一次均值与方差,然后通过标准化公式(X-μ)/σ
转换成标准正太分布查表得出结果。
3、或者更简单的方法是,直接用当前值除以最大值得出百分比。
参考
https://redis.io/topics/data-types-intro
https://redis.io/commands#sorted_set
https://time.geekbang.org/column/article/42896
https://blog.csdn.net/weixin_41140174/article/details/99696028