某海量用戶網站,用戶擁有積分,積分可能會在使用過程當中隨時更新。如今要爲該網站設計一種算法,在每次用戶登陸時顯示其當前積分排名。用戶最大規模爲2億;積分爲非負整數,且小於100萬。mysql
表(user_score)的結構能夠以下設計:算法
Field | Type | Null | Key | Default |
---|---|---|---|---|
uid | int(11) | NO | PRI | 0 |
score | int(11) | NO | 0 |
經過使用一個簡單的SQL語句查詢出積分大於該用戶積分的用戶數量:sql
SELECT 1+ COUNT(t2.uid) AS rank FROM user_score t1, user_score t2 WHERE t1.uid = @uid AND t2.score > t1.score;
缺點:須要對user_score進行全表掃描,還要考慮到查詢的同時如有積分的更新會產生死鎖。海量數據規模以及高併發的應用中不適用。數組
用戶排名是一個全局性的統計指標,而非用戶的私有屬性,緩存在這裏並不適用。緩存
具體的進行問題分析:真實的用戶積分變化是有必定規律的,一般用戶積分都不會暴增暴減。通常用戶都是在低分區,即用戶積分的分佈整體來講是有區段的。同時,高分區用戶的細微變化對低分段用戶排名影響不大。數據結構
考慮按積分區段進行統計方法,引入分區積分表score_range
.併發
Field | Type | Null | Key | Default |
---|---|---|---|---|
from_score | int(11) | NO | PRI | 0 |
to_score | int(11) | NO | PRI | 0 |
count | int(11) | NO | 0 |
該表表示,在積分區間 [from_score, to_score) 有count個用戶高併發
對用戶積分的更新要相應地更新該表的區間值。在score_range
的輔助下,查詢積分爲s的用戶的排名,經過累加高積分區間的count
值,再計算用戶在本區間內的排名便可得到結果。 性能
該方法貌似經過區間聚合減小了查詢計算量。不過有問題是:若是查詢用戶在本區間內的排名呢?優化
SELECT 1+COUNT(t2.uid) AS rank FROM user_score t1, user_score t2 WHERE t1.uid = @uid AND t2.score > t1.score AND t2.score < @to_score;
若是對score
字段創建索引,咱們指望該SQL語句將經過索引大大減小掃描的user_score
表的行數。
不過根據二八定律,對於大量低分區用戶進行區間內排名查詢的性能不及對少數高分區用戶進行查詢。對於通常用戶來說,並無實質性的性能提高。
優勢:經過創建積分區間,減小全表掃描。
缺點:積分分佈的不均勻致使性能並不理想。
再次考慮,是否能夠按照二八定律,把score_range
表設計爲非均勻區間,把低分區劃分密集一點。Eg:開始設置10分一個區間,而後區間逐漸變成100分,1000分……
不過該方法隨機性較大,同時系統的積分分佈會隨着使用而逐漸變化。咱們但願找到一種分區方法,既能夠適應積分非均勻性,又能夠適應系統積分分佈的變化。這就是樹形分區。
咱們把[0, 1m)做爲一級區間,再二分爲兩個2級區間[0, 500k), [500k, 1m),以此類推,最終得到21級區間[0,1), ... , [999999,1m)。
實際上把區間組織爲了平衡二叉樹結構。樹形分區結構須要在更新時保持一種不變量: 非葉子結點的count值 == 左右子節點的count之和
。
每次用戶積分有變化所須要更新的區間數量和積分變化量有關係,積分變化越小更新的區間層次越低。每次須要更新的區間數量是用戶積分變量的log n
級別。
在該積分表的輔助下查詢積分爲s的用戶排名,實際上市在一個區間樹上由上至下明確s所在位置的過程。s積分的排名便是排在他前面的區間的count
的累加。
本算法的更新和查詢都設計若干個操做,但若是爲區間from_score
和to_score
創建索引,這些操做都是基於鍵的查詢和更新,不會產生全表掃描。
同時該算法並不依賴關係數據模型和SQL運算,能夠輕易地改造爲NoSQL。而基於鍵的操做也很容易引入緩存機制進一步優化性能。
估算一下,樹形區間的數目大約爲2billion
個,考慮每一個節點的大小,整個結構只須要幾十m
空間。能夠在內存創建區間樹結構,,經過user_score
表在O(n)
時間內初始化區間樹。
優勢:
不受積分分佈影響;
每次查詢或更新的複雜度爲積分最大值的O(log n)
級別,且與用戶規模無關;
不依賴SQL,容易改造爲內存數據結構
Algo3的時間複雜度只在n特別大的時候才具備優點,而實際應用中積分的變化狀況每每不大,這時和O(n)
算法相比沒有明顯優點。
仔細觀察積分變化對於排名的影響,能夠發現某用戶的積分從s變爲s+n,只有積分在[s, s+n)
區間的用戶排名會降低1名。咱們能夠用一個大小爲1M的數組表示積分和排名的對應關係,rank[s]
表示積分s所對應的排名。初始化時,rank數組能夠由user_score
表在O(n)
的複雜度內計算而來。查詢積分s所對應的排名直接返回rank[s]
便可,當用戶積分從s變爲s+n,只須要把rank[s]
到rank[s+n-1]
這n個元素的值加1便可,複雜度爲O(n)
。
參考
某年某月的《碼農》期刊