redis 用setbit(bitmap)統計活躍用戶

getspool.com的重要統計數據是實時計算的。Redis的bitmap讓咱們能夠實時的進行相似的統計,而且極其節省空間。在模擬1億2千8百萬用戶的模擬環境下,在一臺MacBookPro上,典型的統計如「日用戶數」(dailyunique users) 的時間消耗小於50ms, 佔用16MB內存。Spool如今尚未1億2千8百萬用戶,可是咱們的方案能夠應對這樣的規模。咱們想分享這是如何作到的,也許能幫到其它創業公司。

java

Bitmap以及Redis Bitmaps快速入門(Crash Course on Bitmap and Redis Bitmaps)

Bitmap(即Bitset)
   
Bitmap是一串連續的2進制數字(0或1),每一位所在的位置爲偏移(offset),在bitmap上可執行AND,OR,XOR以及其它位操做。


位圖計數(Population Count)

   
位圖計數統計的是bitmap中值爲1的位的個數。位圖計數的效率很高,例如,一個bitmap包含10億個位,90%的位都置爲1,在一臺MacBook Pro上對其作位圖計數須要21.1ms。SSE4甚至有對整形(integer)作位圖計數的硬件指令。

redis


 
 
 
 
 
 
 
 
Redis Bitmaps
    Redis容許使用二進制數據的Key(binary keys) 和二進制數據的Value(binary values)。Bitmap就是二進制數據的value。Redis的 setbit(key, offset, value)操做對指定的key的value的指定偏移(offset)的位置1或0,時間複雜度是O(1)。



一個簡單的例子:日活躍用戶

    爲了統計今日登陸的用戶數,咱們創建了一個bitmap,每一位標識一個用戶ID。當某個用戶訪問咱們的網頁或執行了某個操做,就在bitmap中把標識此用戶的位置爲1。在Redis中獲取此bitmap的key值是經過用戶執行操做的類型和時間戳得到的。
緩存


      

 

 

 

 

      這個簡單的例子中,每次用戶登陸時會執行一次redis.setbit(daily_active_users, user_id, 1)。將bitmap中對應位置的位置爲1,時間複雜度是O(1)。統計bitmap結果顯示有今天有9個用戶登陸。Bitmap的key是 daily_active_users,它的值是1011110100100101。

    由於日活躍用戶天天都變化,因此須要天天建立一個新的bitmap。咱們簡單地把日期添加到key後面,實現了這個功能。例如,要統計某一天有多少個用戶 至少聽了一個音樂app中的一首歌曲,能夠把這個bitmap的redis key設計爲play:yyyy-mm-dd-hh。當用戶聽了一首歌曲,咱們只是簡單地在bitmap中把標識這個用戶的位置爲1,時間複雜度是 O(1)。

[java]

併發

 

Redis.setbit(play:yyyy-mm-dd, user_id, 1)  

 

 

    今天聽過歌曲的用戶就是key是play:yyyy-mm-dd的bitmap的位圖計數。若是要按周或月統計,只要對這周或這個月的全部bitmap求並集,得出新的bitmap,在對它作位圖計數。
app





 

 

 

    利用這些bitmap作其它複雜的統計也很是容易。例如,統計11月聽過歌曲的高級用戶(premium user):
(play:2011-11-01∪ play:2011-11-02∪ … ∪ play:2011-11-30)∩premium:2011-11
分佈式



1億2千8百萬用戶的性能比較(Performance comparison using 128 million users)高併發

    下面的表格顯示了在1億2千8百萬用戶上完成的時間粒度爲1天,一週,一個月的用戶統計的時間消耗比較。性能

Period Time(ms)
Daily 50.2
Weekly 392.0
Monthly 1624.8




優化(Optimizations)

    前面的例子中,咱們把日統計,周統計,月統計緩存到Redis,以加快統計速度。
   
優化

    這是一種很是靈活的方法。這樣進行緩存的額外紅利是能夠進行更多的統計,如每週活躍的手機用戶—求手機用戶的bitmap與周活躍用戶的交集。或者,若是 要統計過去n天的活躍用戶數,緩存的日活躍用戶使這樣的統計變得簡單——從cache中獲取過去n-1天的日活躍用戶bitmap和今天的bitmap, 對它們作並集(Union),時間消耗是50ms。



示例代碼(SampleCode)

下面的Java代碼用來統計某個用戶操做在某天的活躍用戶。
[java]
spa

 import redis.clients.jedis.Jedis;  
import java.util.BitSet;  
...  
    Jedis redis = new Jedis("localhost");  
    ...  
    public int uniqueCount(String action, String date) {  
        String key = action + ":" + date;  
        BitSet users = BitSet.valueOf(redis.get(key.getBytes()));  
        return users.cardinality();  
    }     

 


下面的Java代碼用來統計某個用戶操做在一個指定多個日期的活躍用戶。
[java] 

 import redis.clients.jedis.Jedis;  
import java.util.BitSet;  
...  
    Jedis redis = new Jedis("localhost");  
    ...  
    public int uniqueCount(String action, String... dates) {  
        BitSet all = new BitSet();  
        for (String date : dates) {  
            String key = action + ":" + date;  
            BitSet users = BitSet.valueOf(redis.get(key.getBytes()));  
            all.or(users);  
        }  
        return all.cardinality();  
    }  

 

 

References:
[1] Redis setbit command


About Author:

      Garyelephant
      garygaowork[at]gmail.com
       關注互聯網創新、分佈式、NOSQL,高併發技術。

相關文章
相關標籤/搜索