需求概述
積分排名在很多項目都會出現,大家都不會陌生,需求也很簡單,積分排名主要滿足以下需求:
- 查詢用戶名次。
- 查詢TopN(即查詢前N名的用戶)
- 實時排名(很多項目是可選的)
當排序的數據量不大的時候,這個需求很容易滿足,但是如果數據量很大的時候比如百萬級、千萬級甚至上億的時候,或者有實時排名需求;這個時候要滿足性能、低成本等需求,在設計上就變得復雜起來了。
常規積分排名處理
這里列舉下日常對於排名的常規做法和缺陷。
1. 數據庫解決方案
這是最簡單的做法,數據存儲在數據庫里面,然后利用數據庫做排序處理。
這里分兩種情況:
1.1單庫/單表
參與排名的數據量小的時候的做法,所有數據存儲在一張表上。
查詢操作示例:
查詢用戶名次:
SELECT count(*) as rank FROM 積分表 WHERE 積分 > (SELECT 積分 FROM 積分表 WHERE uid=’用戶ID’)
查詢前N名:
SELECT uid, 積分 FROM 積分表 ORDER BY 積分 DESC LIMIT 0,N
1.2 分庫/分表
對於這種情況數據不在一塊,在查詢操作上跟上面單表情況的區別就是,分庫/分表需要做,查詢任務切割和查詢結果合並處理。
缺陷:
查詢排名效率低,會造成掃描大量的記錄,甚至全表掃描,性能低,在數據量大、高並發的情況下這種方案是不可用的。
2. 采用常規排序算法
思路上就是把積分排序處理從數據庫轉移出來,自己實現排序和查詢處理。
實際排名業務的特點:
- 每次用戶的積分更新都會在一個小的積分范圍內波動。
- 已有的積分數據都是已排序的。
常見的幾種排序算法大家都熟知這里就不列舉了。
缺陷:
對於海量數據排序處理,簡單的使用常規排序算法並不合適,要么就是排序造成大量的數據移動、要么就是對已排序的數據查詢名次效率不高。
高效的排名算法
前面的排名算法都是針對積分進行排序,然后通過統計積分高於自己的人數獲得排名。
要想知道某個用戶的名次,只需要知道比這個用戶高分的人數,不一定需要對積分做排序。
在這里換個思路不對積分進行排序,僅僅是統計每個積分區間的人數,用積分區間的形式去統計相應的人數,下面是算法描述。
1. 根據積分范圍創建平衡二叉樹。
設[0, N]為積分范圍, 構造的平衡二叉樹如下圖。
每個節點包含兩個數據字段(除了指針):
Range: 表示積分范圍。
Counts: 表示當前積分區間包含多少人。
積分的區間的划分是根據平分的方式,把當前積分范圍一分為二生成兩個子節點,然后遞歸的重復該步驟,直到積分區間無法划分為止(即區間[x, y], x == y)
例子:
假設積分范圍為: [0, 5], 構造的平衡二叉樹如下圖:
節點內的數據表示當前積分區間的人數。
從上圖可以看出來,所有積分都在葉子節點,葉子節點即最小粒度的積分區間。
2. 統計相應積分區間的人數。
這里主要有兩種操作:
假設積分為i
- 添加積分
添加積分的過程就是查找積分i, 同時累加查找過程經過的節點計數。
下面給出操作例子,注意觀察操作路徑。
例: 需要添加積分3, 結果如下圖
接着在添加積分4,結果如下圖
接着再添加積分4,結果如下圖
接着添加積分2,結果如下圖
- 刪除積分
刪除積分的過程也是查找積分i, 區別是查找過程經過的節點計數全部減1。
Ps: 只有積分是存在的情況下,才能做刪除操作,另外用一組標記,標識積分是否存在,這里就不列舉了。
例子: 刪除積分4, 結果如下圖
3. 查詢名次操作。
查詢某個積分的排名的過程也是查找積分i的過程,下面是查找過程統計節點計數的算法:
對於查找路徑上的任意節點,如果積分在左節點區間,則累加右節點區間的計數。
最終累加計數的結果加1即是積分的名次
例子: 查找積分3的名次
藍色節點是查找積分3經過的路徑,紅色節點是需要累加的計數值。
最終結果是:0 + 1 + 1, 積分3的名次是第2名
從上面的算法可以看出,對平衡二叉樹的操作,算法復雜度是O(log N), N是最大積分。
在積分范圍不變的情況下,算法復雜度是穩定的,跟用戶量無關,因此可以實現海量用戶積分排名、實時排名算法需要。
對於海量積分數據實時排名、這里給出的是核心算法,實際業務的時候還需要增加一些額外的處理,比如uid於積分的映射表用於記錄用戶歷史積分、積分與uid的映射表用於TopN這種查詢前N名的需求、數據持久化、高可用等需求。