如何做到單機毫秒完成上億規(guī)模大數(shù)據(jù)常規(guī)統(tǒng)計
雖然現(xiàn)在最火的是AI,但是大數(shù)據(jù)和計算能力仍然是機器學(xué)習(xí)/AI算法的重要支撐,我們的業(yè)務(wù)場景大部分是通過手機終端、服務(wù)器日志不斷產(chǎn)生日志數(shù)據(jù),通過消息通道發(fā)送到大數(shù)據(jù)平臺進行存儲、加工和統(tǒng)計,然后在統(tǒng)計數(shù)據(jù)之上提供算法挖掘用戶偏好行為和畫像,為此,我們的關(guān)鍵任務(wù)是需要從海量數(shù)據(jù)里統(tǒng)計分析每項產(chǎn)品的去重用戶、新增用戶、pv、uv、dau(日活)、mau(月活)等指標(biāo),這個過程存儲占用越少,計算時間越快越好。Fourinone(CoolHash)擁有原創(chuàng)數(shù)據(jù)庫引擎設(shè)計能力和知識產(chǎn)權(quán),能夠在引擎層面靈活擴充各種功能支持,為了提供大數(shù)據(jù)統(tǒng)計計算的***解決方案,4.17在引擎上增強了以下特性:
一、增加了自加和存在新增兩個原子操作
1. Object putPlus(String key, T plusValue)
如果key對應(yīng)的value是數(shù)字類型(int、long、double、float),自增加plusValue(數(shù)字類型),如plusValue=1,表示每次自增1,plusValue也可以是小數(shù)。如果key對應(yīng)的value是字符串類型,自增加plusValue(字符串),會累加到原字符串后面,可以用分隔符隔開。putPlus的返回值為該key的上一個值。
2. Object putNx(String key, T value)
如果key存在,則不操作,如不存在寫入value。putNx返回值為key操作前值,為null表示不存在,否則返回已有值。
利用putPlus和putNx可以完成很多原子操作,如count類計數(shù)統(tǒng)計,在開源包指南附帶的CountDemo.java里的countTest方法演示了putPlus的使用,在ThreadClient.java的putPlusTest方法和putNxTest方法演示了多線程下的使用。
pvTest方法演示了計算pv,如果id不存在則寫入,并將pv數(shù)自加1,其他線程發(fā)現(xiàn)id存在,則無法更新pv數(shù)
- Object nx = chc.putNx("v0_"+i, i);
- if(nx==null)
- chc.putPlus("pv_v0",1);
二、增加了客戶端本地和存儲引擎端強大的bitmap支持
上面通過putPlus和putNx原子操作可以計算pv,但并不是***效的方案,使用bitmap有兩個非常顯著的優(yōu)勢:位存儲占用空間低,位計算效率高。將需要做統(tǒng)計計算的id轉(zhuǎn)換成數(shù)字序號,每個只占1個bit,對于20億的用戶id,只需要20億bit約238m大小,壓縮后占用空間更小,最少只要200k;通過單個bitmap可以完成去重操作,通過多個bitmap的且、或、異或、反等位操作可以完成日活、月活、小時分鐘活躍、重度用戶、新增用戶、用戶流向等絕大部分的統(tǒng)計計算,而且能在單機毫秒級完成,真正做到實時計算出結(jié)果,同比hadoop/hive離線計算執(zhí)行“select distinct count…from…groupby join…”類似sql的方式統(tǒng)計,往往需要幾百臺機器,耗用30分鐘才能完成,對比非常懸殊,而且容易形成大量sql任務(wù)調(diào)度和大表join給集群帶來繁重壓力。(圖)
- 去重用戶:求1的總數(shù)
- 活躍用戶:取或bitmap1 | bitmap2
- 非活躍用戶:取反:~bitmap1
- 重度用戶:取且:Bitmap1 & bitmap2
- 新增用戶:取或加異或:(Bitmap1 | bitmap2)^bitmap1
- 多種指標(biāo)組合:Bitmap1 & bitmap2 & bitmap3 &…
- 等等
同時提供bitmap本地和引擎端互通實現(xiàn),能夠進行更靈活的架構(gòu)設(shè)計,可以將bitmap壓縮存儲到任何數(shù)據(jù)庫上,客戶端拉回后完成聚合計算,計算完成的結(jié)果再寫回數(shù)據(jù)庫。也可以多個客戶端同時連接到CoolHash存儲引擎上,通過引擎的bitmap操作支持完成去重、聚合、解壓縮等支持。BitMap結(jié)合存儲引擎如下圖:
1. 本地內(nèi)存實現(xiàn),CoolBitSet實現(xiàn)了以下bitmap功能:
CoolBitSet(int maxSize),可指定大小限制,默認(rèn)1000萬大小,本地沒有***限制,可以使用多個分區(qū)的bitmap表示整型范圍或長整型范圍的數(shù)據(jù),每個1000萬的bitmap壓縮后在2m以內(nèi),很適合放入kv存儲。
(1)基本操作:CoolBitSet提供基本的get(int n)、set(int n)、put(int n)操作,其中put為存在返回get,不存在set,除外還提供批量操作:int set(CoolBitSet cbs): 將另外一個bitmap對象合并到當(dāng)前bitmap,并返回新增的數(shù)量。
(2)聚合操作:求且、求或、異或、求反、求新增
- CoolBitSet and(CoolBitSet cbs):兩個CoolBitSet求且,更新到當(dāng)前對象,并返回該對象引用
- CoolBitSet or(CoolBitSet cbs):兩個CoolBitSet求或,同上
- CoolBitSet xor(CoolBitSet cbs):兩個CoolBitSet求異或,同上
- CoolBitSet andnot():將該CoolBitSet對象求反,同上
- CoolBitSet setNew(CoolBitSet cbs):求當(dāng)前CoolBitSet的新增用戶,并返回新增用戶結(jié)果的對象引用
(3)求總數(shù):int getTotal()返回該CoolBitSet的用戶總數(shù),bit位是1的總數(shù)量
(4)求容量:int getSize()返回該CoolBitSet的容量大小
(5)調(diào)試查看:String toString(int num)返回該CoolBitSet的二進制字符串,為了減少長度,參數(shù)num為需要查看的byte數(shù),如num=5表示查看前5個byte的二進制串
和java的bitmap的實現(xiàn)區(qū)別:jdk自帶的BitSet類是以long數(shù)組實現(xiàn),而且只能初始化大小,無法限制大小,每個bitset要耗用幾百m的內(nèi)存,多個bitmap容易造成空間大量浪費,BitSet類只是本地內(nèi)存實現(xiàn),沒有分布式存儲引擎持久化支持。
2. 引擎端持久化實現(xiàn),CoolHashClient提供了以下接口用來操作存儲引擎:
(1)int putBitSet(String key, int index):
- 單項操作,類似CoolBitSet的put,***個參數(shù)為bitmap的key,第二個參數(shù)將該bitmap的index位置設(shè)為1。
(2)boolean getBitSet(String key, int index):
- 單項操作,類似CoolBitSet的get,***個參數(shù)為bitmap的key,第二個參數(shù)需要獲取的index位置的值。
(3)int putBitSet(String key, CoolBitSet cbs):
- 批量操作,類似CoolBitSet的批量set,將另外一個bitmap對象合并到指定key的bitmap,并返回新增的數(shù)量。獲取CoolBitSet對象仍然使用get接口Object get(String key)
(4)Object putBitSet(String key, CoolBitSet cbs, String logical):
- 聚合操作,參數(shù)logical可以設(shè)置為“and”,“or”,“xor”,“andnot”,”new”之一,對于“andnot”,參數(shù)cbs并不起作用,可以傳入任意不為空的CoolBitSet對象。聚合操作會作用到該key指定的bitmap上,返回值為聚合后的CoolBitSet對象。
以上操作遵循CoolHash的k/v存儲約束,k為字符串,v不超過2m(可修改默認(rèn)配置大小)。
注意CoolBitSet對象可以用三種方式進行k/v存儲和壓縮:
- 存儲為bitSet格式,合并數(shù)據(jù):putBitSet(String key, CoolBitSet cbs)
- 存儲為bitSet格式,直接覆蓋:put(String key, CoolBitSet cbs)
- 普通kv存儲格式,非bitSet格式:put(String key, cbs.getBytes());
由于是對象存儲,三種put方式都會對value數(shù)據(jù)進行壓縮,采用壓縮率和耗時比較平衡的gzip壓縮。
前兩種bitSet格式存儲方式,會驗證CoolBitSet大小不能超過1億,否則不能提交。
第三種普通kv存儲格式,沒有1億的限制,只要壓縮后大小不超過2m,可以正常提交,但由于不是CoolBitSet格式,存儲引擎無法識別做聚合等操作。
和redis的bitmap的實現(xiàn)區(qū)別:redis實現(xiàn)了bitmap的單項操作和聚合操作,但是沒有批量操作,也沒有壓縮,通過offset指定偏移量的方式分配空間容易造成浪費。
開源包指南附帶CountDemo.java里的演示:
bitSetTest方法:先演示了全量存儲,寫入10億數(shù)據(jù)到1個bitmap,耗時不到1秒;再演示了分區(qū)存儲,將1億大小的數(shù)據(jù)分成10個1000萬大小的bitmap存儲。
realtimeStatistics方法:演示基于bitmap做用戶去重、活躍用戶、非活躍用戶、重度用戶、新增用戶等實時計算。
retainLocal方法和retainServer方法:
分別演示了如何使用本地內(nèi)存和存儲引擎計算用戶留存。
3. 增加String類型的bitmap支持:
StringBitMap實現(xiàn)了String類型的bitMap,通過對hash算法的改進,能夠做到1億字符串?dāng)?shù)據(jù)只有200多的碰撞率,5000萬內(nèi)數(shù)據(jù)幾乎沒有碰撞率,對于不超過1億的數(shù)據(jù)是很合適的,但1億以上的字符串?dāng)?shù)量仍然不合適,碰撞率會大幅上升。開源包指南附帶CountDemo.java里的stringBitMapTest方法演示了模擬1000萬隨機生成的15位IMEI設(shè)備號,并返回碰撞個數(shù)。
4. 17.10版本同時提供jdk1.8.0_151編譯下”fourinone.jar”包和jdk1.7.0_80編譯下”fourinone-jdk7.jar”包。4.17.10版本更新github code和gitee code,本版本所有開源內(nèi)容已經(jīng)進行了公司報備,感謝對開源的支持。