前言
排行榜幾乎已經成為互聯網應用中的必備模塊,特別是游戲領域,它是對某一相關同類事物的客觀實力的反映,帶有相互之間的比較性質,帶有競爭意義。
對於平台來說,可以帶來一定的權威性,提高平台影響力。
對於商家來說,可以帶來更多的曝光,並對比自身的不足加以改進。
對於用戶來說,可以為行動決策做參考,降低相關風險成本。
那么排行榜如何實現?我將結合自身的經驗提供一些簡單設計思路。
基於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