背景
此為我當初經歷過的一個電商項目里的場景,當時經歷了幾波大促用戶總注冊量在億級,然后為了進一步推廣就有了這樣一個需求: 拉取用戶手機的通訊錄列表,判斷其中的未注冊手機號,展示邀請按鈕.
問題整理
現有的查詢是通過手機號查詢用戶信息,但如果僅僅是判斷用戶是否存在而查出整個用戶信息略顯浪費,另外最重要的是通訊錄里大部分手機號一般來說都是未注冊的,這個時候會有明顯的緩存穿透問題.
這種快速判斷是否存在我的第一反應是記錄個以手機號為key的緩存,但是考慮到量級我大致算了一下占用容量於是我放棄了,一億手機號保守估計占用5.6g,我們是集群的一億手機號還好,但是如果有三/四億的話就很浪費內存資源了擴展性十分不理想(事實上三四億過了段時間就達到了). 容量占用計算方式參考點此
一個dict entry占用 = 24字節 , 一個 key占用 = sds = 9字節 + 字符串內容字節數 , 一個redisObject = 16字節 + value內容
也就說一個redis kv對要占用 49字節 + key內容 + value內容
key 絕大多數為11位手機號,大約占用11字節.
故 60字節 左右為一個手機號的kv對所占用內容.
1億手機號就是60億字節 = 5.58g左右
4億就要22g+了
目標
- 不需要查出整個用戶信息,僅需要判斷是否存在即可. 目的: 節省io開銷
- 不能讓大量請求直接查庫,盡可能走緩存. 目的: 節省數據庫io資源
- 緩存數據量占用不可太大. 目的: 節省緩存(內存)存儲資源
方案調研
bitmap標記
快速排除掉簡單用key來標記的方案后,bitmap是我馬上能想到的下一個方案.
常見手機號11位,假如出現最大值需要千億偏移量,即需要千億byte(約90+G)的空間來存儲,我們使用的redis. 但是手機號有個特色:並不是均勻分布而是集中分布的,比如杭州移動號段前綴一般是 136,137,138,139...
所以我在這里進行了bitmap分區,沒有使用到的號段就不會生成bitmap,然后一個bitmap區間我拍了個腦袋定了個1MB大小即每8388608個號碼使用一個bitmap分區.
偽代碼
public static class IsPhoneExists {
//低位部分的偏移量
private static final int LOW_OFFSET = 23;
//用於跟手機號求與算出offset
private static final long LOWWER_MOD = (1 << LOW_OFFSET) - 1;
//與手機號求與算出高位部分(不過最后還是要右移,這一步可沒有)
private static final long PREFIX_MOD = ~LOWWER_MOD;
//計算分區key
public static String getKey(long phone) {
return String.format("phone_bitmap_%d",(phone & PREFIX_MOD) >>> LOW_OFFSET);
}
//計算偏移量
public static int getOffset(long phone) {
return (int) (phone & LOWWER_MOD);
}
}
布隆過濾器
布隆過濾器(點此查看)是網上緩存穿透解決方案常客.
這里也簡單評估下設計量級,若使用basic bloom可以參考上面布隆過濾器介紹中的公式計算搜需要的空間(m為布隆過濾器空間,n為數據量,p為誤判率).
需要的hash函數個數k則除了上圖公式外哈弗還有篇論文則認為只要有2個就夠了Building a Better Bloom Filter
需要空間m=y*n跟p的關系
誤判率想要達到1%以內,需要的m = 9.59n左右就是近似10倍於數據量的bitmap空間. 10%則是4.8n即5倍左右
由於我們場景存在手機號號段分布比較集中的特點且布隆過濾器為了保證查詢結果的准確性,還是得去查一次庫.,所以這里我最終選擇了bitmap方案.
我一個無中生友遇到的場景則是更適用於布隆過濾器,他們的id是設備號這種分布范圍十分大且沒啥規律的字符串,但總量也是在億級別.所以他們無法使用bitmap這種標記方式.
布谷鳥過濾器
看了一下,實現起來有點費勁,棄了.
- 布谷鳥hash解決hash沖突的實現成本較高.
- 數據存儲相較於原始值,僅僅是優化成了指紋的大小,空間占用問題上並沒有太大的質變.
- 仍有誤判率存在.
最后結論
bitmap走起.