這篇可參考html
http://www.cnblogs.com/weidagang2046/archive/2012/03/01/massive-user-ranking.html#!comments面試
某海量用戶網站,用戶擁有積分,積分可能會在使用過程當中隨時更新。如今要爲該網站設計一種算法,在每次用戶登陸時顯示其當前積分排名。用戶最大規模爲2億;積分爲非負整數,且小於100萬。算法
PS: 聽說這是迅雷的一道面試題,不過問題自己具備很強的真實性,因此本文打算按照真實場景來考慮,而不侷限於面試題的理想環境。sql
首先,咱們用一張用戶積分表user_score來保存用戶的積分信息。數據庫
表結構:數組
示例數據:緩存
下面的算法會基於這個基本的表結構來進行。數據結構
首先,咱們很容易想到用一條簡單的SQL語句查詢出積分大於該用戶積分的用戶數量:併發
select 1 + count(t2.uid) as rank from user_score t1, user_score t2 where t1.uid = @uid and t2.score > t1.score
對於4號用戶咱們能夠獲得下面的結果:高併發
算法特色
優勢:簡單,利用了SQL的功能,不須要複雜的查詢邏輯,也不引入額外的存儲結構,對小規模或性能要求不高的應用不失爲一種良好的解決方案。
缺點:須要對user_score表進行全表掃描,還須要考慮到查詢的同時如有積分更新會對錶形成鎖定,在海量數據規模和高併發的應用中,性能是沒法接受的。
在許多應用中緩存是解決性能問題的重要途徑,咱們天然會想能不能把用戶排名用Memcached緩存下來呢?不過再一想發現緩存彷佛幫不上什麼忙,由於用戶排名是一個全局性的統計性指標,而並不是用戶的私有屬性,其餘用戶的積分變化可能會立刻影響到本用戶的排名。然而,真實的應用中積分的變化其實也是有必定規律的,一般一個用戶的積分不會忽然暴增暴減,通常用戶老是要在低分區混跡很長一段時間纔會慢慢升入高分區,也就是說用戶積分的分佈整體說來是有區段的,咱們進一步注意到高分區用戶積分的細微變化其實對低分段用戶的排名影響不大。因而,咱們能夠想到按積分區段進行統計的方法,引入一張分區積分表score_range:
表結構:
數據示例:
表示[from_score, to_score)區間有count個用戶。若咱們按每1000分劃分一個區間則有[0, 1000), [1000, 2000), …, [999000, 1000000)這1000個區間,之後對用戶積分的更新要相應地更新score_range表的區間值。在分區積分表的輔助下查詢積分爲s的用戶的排名,能夠首先肯定其所屬區間,把高於s的積分區間的count值累加,而後再查詢出該用戶在本區間內的排名,兩者相加便可得到用戶的排名。
乍一看,這個方法貌似經過區間聚合減小了查詢計算量,實則否則。最大的問題在於如何查詢用戶在本區間內的排名呢?若是是在算法1中的SQL中加上積分條件:
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
在理想狀況下,因爲把t2.score的範圍限制在了1000之內,若是對score字段創建索引,咱們指望本條SQL語句將經過索引大大減小掃描的user_score表的行數。不過真實狀況並不是如此,t2.score的範圍在1000之內並不意味着該區間內的用戶數也是1000,由於這裏有積分相同的狀況存在!二八定律告訴咱們,前20%的低分區每每集中了80%的用戶,這就是說對於大量低分區用戶進行區間內排名查詢的性能遠不及對少數的高分區用戶,因此在通常狀況下這種分區方法不會帶來實質性的性能提高。
算法特色
優勢:注意到了積分區間的存在,並經過預先聚合消除查詢的全表掃描。
缺點:積分非均勻分佈的特色使得性能提高並不理想。
均勻分區查詢算法的失敗是因爲積分分佈的非均勻性,那麼咱們天然就會想,能不能按二八定律,把score_range表設計爲非均勻區間呢?好比,把低分區劃密集一點,10分一個區間,而後逐漸變成100分,1000分,10000分 … 固然,這不失爲一種方法,不過這種分法有必定的隨意性,不容易把握好,並且整個系統的積分分佈會隨着使用而逐漸發生變化,最初的較好的分區方法可能會變得不適應將來的狀況了。咱們但願找到一種分區方法,既能夠適應積分非均勻性,又能夠適應系統積分分佈的變化,這就是樹形分區。
咱們能夠把[0, 1,000,000)做爲一級區間;再把一級區間分爲兩個2級區間[0, 500,000), [500,000, 1,000,000),而後把二級區間二分爲4個3級區間[0, 250,000), [250,000, 500,000), [500,000, 750,000), [750,000, 1,000,000),依此類推,最終咱們會獲得1,000,000個21級區間[0,1), [1,2) … [999,999, 1,000,000)。這其實是把區間組織成了一種平衡二叉樹結構,根結點表明一級區間,每一個非葉子結點有兩個子結點,左子結點表明低分區間,右子結點表明高分區間。樹形分區結構須要在更新時保持一種不變量(Invariant):非葉子結點的count值老是等於其左右子結點的count值之和。
之後,每次用戶積分有變化所須要更新的區間數量和積分變化量有關係,積分變化越小更新的區間層次越低。整體上,每次所須要更新的區間數量是用戶積分變量的log(n)級別的,也就是說若是用戶積分一次變化在百萬級,更新區間的數量在二十這個級別。在這種樹形分區積分表的輔助下查詢積分爲s的用戶排名,其實是一個在區間樹上由上至下、由粗到細一步步明確s所在位置的過程。好比,對於積分499,000,咱們用一個初值爲0的排名變量來作累加;首先,它屬於1級區間的左子樹[0, 500,000),那麼該用戶排名應該在右子樹[500,000, 1,000,000)的用戶數count以後,咱們把該count值累加到該用戶排名變量,進入下一級區間;其次,它屬於3級區間的[250,000, 500,000),這是2級區間的右子樹,因此不用累加count到排名變量,直接進入下一級區間;再次,它屬於4級區間的…;直到最後咱們把用戶積分精肯定位在21級區間[499,000, 499,001),整個累加過程完成,得出排名!
雖然,本算法的更新和查詢都涉及到若干個操做,但若是咱們爲區間的from_score和to_score創建索引,這些操做都是基於鍵的查詢和更新,不會產生表掃描,所以效率更高。另外,本算法並不依賴於關係數據模型和SQL運算,能夠輕易地改造爲NoSQL等其餘存儲方式,而基於鍵的操做也很容易引入緩存機制進一步優化性能。進一步,咱們能夠估算一下樹形區間的數目大約爲2,000,000,考慮每一個結點的大小,整個結構只佔用幾十M空間。因此,咱們徹底能夠在內存創建區間樹結構,並經過user_score表在O(n)的時間內初始化區間樹,而後排名的查詢和更新操做均可以在內存進行。通常來說,一樣的算法,從數據庫到內存算法的性能提高經常能夠達到10^5以上;所以,本算法能夠達到很是高的性能。
算法特色
優勢:結構穩定,不受積分分佈影響;每次查詢或更新的複雜度爲積分最大值的O(log(n))級別,且與用戶規模無關,能夠應對海量規模;不依賴於SQL,容易改造爲NoSQL或內存數據結構。
缺點:算法相對更復雜。
算法3雖然性能較高,達到了積分變化的O(log(n))的複雜度,可是實現上比較複雜。另外,O(log(n))的複雜度只在n特別大的時候才顯出它的優點,而實際應用中積分的變化狀況每每不會太大,這時和O(n)的算法相比每每沒有明顯的優點,甚至可能更慢。
考慮到這一狀況,仔細觀察一下積分變化對排名的具體影響,能夠發現某用戶的積分從s變爲s+n,積分小於s或者大於等於s+n的其餘用戶排名實際上並不會受到影響,只有積分在[s,s+n)區間內的用戶排名會降低1位。咱們能夠用於一個大小爲1,000,000的數組表示積分和排名的對應關係,其中rank[s]表示積分s所對應的排名。初始化時,rank數組能夠由user_score表在O(n)的複雜度內計算而來。用戶排名的查詢和更新基於這個數組來進行。查詢積分s所對應的排名直接返回rank[s]便可,複雜度爲O(1);當用戶積分從s變爲s+n,只須要把rank[s]到rank[s+n-1]這n個元素的值增長1便可,複雜度爲O(n)。
算法特色
優勢:積分排名數組比區間樹更簡單,易於實現;排名查詢複雜度爲O(1);排名更新複雜度O(n),在積分變化不大的狀況下很是高效。
缺點:當n比較大時,須要更新大量元素,效率不如算法3。
上面介紹了用戶積分排名的幾種算法,算法1簡單易於理解和實現,適用於小規模和低併發應用;算法3引入了更復雜的樹形分區結構,可是O(log(n))的複雜度性能優越,能夠應用於海量規模和高併發;算法4採用簡單的排名數組,易於實現,在積分變化不大的狀況下性能不亞於算法3。本問題是一個開放性的問題,相信必定還有其餘優秀的算法和解決方案,歡迎探討!