<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
在巨量資料時代,想要不斷提升基於海量資料獲取的決策、洞察發現和流程優化等能力,就需要不停思考,如何在利用有限的資源實現高效穩定地產出可信且豐富的資料,從而提高賦能下游產品的效率以及效果。在貨拉拉數倉構建過程中,我們不斷探索各種方式來實現降本提效。例如在一些場景下,利用Bitmap去提升下游的資料使用體驗,並達成我們想要的降本提效的目的。
為了更好的展示Bitmap在貨拉拉實踐應用中的探索與實踐,我們分了兩篇文章來介紹,本文主要介紹Bitmap的實現原理與應用優化,以及在一些常見業務場景中的實踐應用,讓大家在面對相應場景的時候,能夠有一個區別於傳統的解決方案的新思路。下一篇文章則是重點介紹Bitmap在貨拉拉中的具體落地,以及使用時遇到的一些痛點和對應解決方案。
首先從Bitmap開始說起,這裡採用經典的WWH結構,讓不熟悉的小夥伴能夠快速瞭解掌握。
BitMap,即點陣圖,是比較常見的資料結構,簡單來說就是按位元儲存,主要為了解決在去重場景裡面巨量資料量儲存的問題。本質其實就是雜湊表的一種應用實現,使用每個位來表示某個數位。
舉個例子
假設有個1,3,5,7的數位集合,如果常規的儲存方法,要用4個Int的空間。其中一個Int就是32位元的空間。3個就是4*32Bit,相當於16個位元組。
如果用Bitmap儲存呢,只用8Bit(1個位元組)就夠了。bitmap通過使用每一bit位代表一個數,位號就是數值,1標識有,0標識無。如下所示:
對於 BitMap 這種經典的資料結構,在 Java 語言裡面,其實已經有對應實現的資料結構類 java.util.BitSet 了,而 BitSet 的底層原理,其實就是用 long 型別的陣列來儲存元素,因為使用的是long型別的陣列,而 1 long = 64 bit,所以資料大小會是64的整數倍。這樣看可能很難理解,下面參考bitmap原始碼寫了一個例子,並寫上了詳細的備註,方便理解
import java.util.Arrays; public class BitMap { // 用 byte 陣列儲存資料 private byte[] bits; // 指定 bitMap的長度 private int bitSize; // bitmap構造器 public BitMap(int bitSize) { this.bitSize = (bitSize >> 3) + 1; //1byte 能儲存8個資料,那麼要儲存 bitSize的長度需要多少個bit呢,bitSize/8+1,右移3位相當於除以8 bits = new byte[(bitSize >> 3) + 1]; } // 在bitmap中插入數位 public void add(int num) { // num/8得到byte[]的index int arrayIndex = num >> 3; // num%8得到在byte[index]的位置 int position = num & 0x07; //將1左移position後,那個位置自然就是1,然後和以前的資料做|,這樣,那個位置就替換成1了。 bits[arrayIndex] |= 1 << position; } // 判斷bitmap中是否包含某數位 public boolean contain(int num) { // num/8得到byte[]的index int arrayIndex = num >> 3; // num%8得到在byte[index]的位置 int position = num & 0x07; //將1左移position後,那個位置自然就是1,然後和以前的資料做&,判斷是否為0即可 return (bits[arrayIndex] & (1 << position)) != 0; } // 清除bitmap中的某個數位 public void clear(int num) { // num/8得到byte[]的index int arrayIndex = num >> 3; // num%8得到在byte[index]的位置 int position = num & 0x07; //將1左移position後,那個位置自然就是1,然後對取反,再與當前值做&,即可清除當前的位置了. bits[arrayIndex] &= ~(1 << position); } // 列印底層bit儲存 public static void printBit(BitMap bitMap) { int index=bitMap.bitSize & 0x07; for (int j = 0; j < index; j++) { System.out.print("byte["+j+"] 的底層儲存:"); byte num = bitMap.bits[j]; for (int i = 7; i >= 0; i--) { System.out.print((num & (1 << i)) == 0 ? "0" : "1"); } System.out.println(); } } // 輸出陣列元素,也可以使用Arrays的toString方法 private static void printArray(int[] arr) { System.out.print("陣列元素:"); for (int i = 0; i < arr.length; i++) { System.out.print(arr[i]+" "); } System.out.println(); } }
下面就來簡單玩一玩這個自制的BitMap,先嚐試插入一個3,並清理掉它,看看底層二進位制結構是怎樣變化的
public static void main(String[] args) { // 簡單試驗 BitMap bitmap = new BitMap(3); bitmap.add(3); System.out.println("插入3成功"); boolean isexsit = bitmap.contain(3); System.out.println("3是否存在:" + isexsit); printBit(bitmap); bitmap.clear(3); isexsit = bitmap.contain(3); System.out.println("3是否存在:" + isexsit); printBit(bitmap); }
輸出結果如下:
再通過陣列,插入多個元素看看效果
public static void main(String[] args) { // 陣列試驗 int[] arr = {8,3,3,4,9}; printArray(arr); int size = Arrays.stream(arr).max().getAsInt(); BitMap b = new BitMap(size); for (int i = 0; i < arr.length; i++) { b.add(arr[i]); } printBit(b); }
輸出結果如下:
前面簡單瞭解了一下BitMap,下面就通過原始碼來看看java是如何實現BitSet的。
開啟原始碼,首先映入眼簾的是下面這一段長長的備註,簡單翻譯一下,便於英語不好的小夥伴理解
原始碼備註翻譯如下
首先可以看到原始碼中,最核心的屬性資訊。在BitSet 中使用的是long[] 作為底層儲存的資料結構,並通過一個 int 型別的變數,來記錄當前已經使用陣列元素的個數。
這種型別的屬性結構很常見,比如StringBuilder、StringBuffer底層是一個char[] 作為儲存,一個int 變數用來計數,相似的還有ArrayList,Vector等
/** * The internal field corresponding to the serialField "bits". */ private long[] words; /** * The number of words in the logical size of this BitSet. */ private transient int wordsInUse = 0;
再往下看,是一個很重要的方法,是用來獲取某個數在陣列中的下標,採用的演演算法是將這個數右移6位,這是因為 bitIndex >> 6 = bitIndex / (2^6) = bitIndex /64,而long就是64個位元組
private final static int ADDRESS_BITS_PER_WORD = 6; /** * Given a bit index, return word index containing it. */ private static int wordIndex(int bitIndex) { return bitIndex >> ADDRESS_BITS_PER_WORD; }
接著比較有意思的就是它的空參構造器,BITS_PER_WORD預設是1<<6 也就是64,根據上面方法原理,wordIndex(64-1)+1 = 1 ,所以最終初始化的是長度為1的陣列
private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; /** * Creates a new bit set. All bits are initially { @code false}. */ public BitSet() { initWords(BITS_PER_WORD); sizeIsSticky = false; } private void initWords(int nbits) { words = new long[wordIndex(nbits-1) + 1]; }
最後看到這個很經典也很重要的方法,由於底層是陣列,在初始化的時候,並不知道將來會需要儲存多大的資料,所以對於這一類底層核心實現結構是陣列的實體類,通常會使用動態擴容的方法,具體實現細節也都大同小異,這裡實現的動態擴容是原本的兩倍,和Vector類似。
/** * Ensures that the BitSet can hold enough words. * @param wordsRequired the minimum acceptable number of words. */ private void ensureCapacity(int wordsRequired) { // 如果陣列的長度小於所需要的就要進行擴容 if (words.length < wordsRequired) { // Allocate larger of doubled size or required size // 擴容最終的大小,最小為原來的兩倍 int request = Math.max(2 * words.length, wordsRequired); // 建立新的陣列,容量為request,然後將原本的陣列拷貝到新的陣列中 words = Arrays.copyOf(words, request); // 並設定陣列大小不固定 sizeIsSticky = false; } }
至於其他的原始碼細節,因為篇幅有限,就暫且不表,感興趣的可以自行閱讀~
根據bitmap的實現原理,其實可以總結出使用bitmap的幾個主要原因:
雖然bitmap有諸多好處,但是正所謂人無完人,它也存在很多缺陷。
既然bitmap的優點如此突出,那應該如何去優化它存在的一些侷限呢?
為了快速把這個原理說清楚,這裡就不繼續擼原始碼了,有興趣的小夥伴可以自行搜尋相關原始碼閱讀,下面簡單闡述一下它的核心原理:1個Int 型別相當於有32 bit 也就相當於2^32=2^16 x 2^16,這意味著任意一個Int 型別可以拆分成兩個16bit的來儲存,每一個拆出來的都不會大於2^16, 2^16就是65536,而Int的正整數實際最大值為 2147483647。而RoaringBitmap的壓縮首先做的就是用原本的數去除65536,結果表示成(商,餘數),其中商和餘數是都不會超過65536。
如下圖所示
RoaringBitmap的做法就是將131138 原本32bit的儲存結構,拆分成連兩個16bit的結構,而拆分出的兩個16bit分別儲存了131138除65536的商2以及餘數66。
在RoaringBitmap中,把商所處的16bit 被稱為高16位元,除數所處的16bit 被稱為低16位元。並用key和Container去儲存的這些拆分出來的資料,其中key是short[] ,存放的就是商,因為bitmap的特性,商和餘數不會存在完全相同的情況。
通過商來作為key劃分不同的Container,就類似劃分不同的桶,key就是標識資料應該存在哪個桶,container用來存對應資料的低16位元的數位。比如 1000和60666 除以65536後的結果分別是(0,1000)和(0,60666),所以這兩個資料儲存到RoaringBitmap中,就會都放到key位0那個container中,container中就是1000和60666。
由於container中存放的數位是0~65536的一些資料,可能稀疏可能稠密,所以RoaringBitmap依據不同的場景,提供了 3 種不同的 Container,分別是 ArrayContainer 、 BitmapContainer 、RunContainer。
關於三個Container的儲存原理如下:
關於ArrayContainer和BitmapContainer的選擇:
如圖所示,可以看到ArrayContainer 更適合存放稀疏的資料,BitmapContainer 適合存放稠密的資料。在RoaringBitmap中,若一個 Container 裡面的元素數量小於 4096,會使用 ArrayContainer 來儲存。當 Array Container 超過最大容量 4096 時,會轉換為 BitmapContainer,這樣能夠最大化的優化儲存。
bitmap就像一柄雙刃劍,用的好可以幫助我們破除瓶頸,解決痛點。用的不好不僅會丟失它所有的優點,還要搭上過多的儲存,甚至會喪失掉最重要的準確性,所以要針對不同業務場景靈活使用我們的武器,才能事半功倍!
下面舉例bitmap的一些使用場景,來看看實際開發中,到底怎麼正確使用bitmap:
假設有使用者的標籤寬表,對應欄位及值如下
user_id(使用者id) | city_id(城市id) | is_user_start(是否啟動) | is_evl(是否估價) | is_order(是否下單) |
---|---|---|---|---|
1 | 1001 | 1 | 1 | 1 |
2 | 1001 | 1 | 1 | 0 |
3 | 1002 | 1 | 1 | 1 |
4 | 1002 | 1 | 0 | 0 |
5 | 1003 | 0 | 0 | 0 |
如果想根據標籤劃分人群,比如:1001城市 + 下單。
通常是對列值進行遍歷篩選,如果優化也就是列上建立索引,但是當這張表有很多標籤列時,如果要索引生效並不是每列有索引就行,要每種查詢組合建一個索引才能生效,索引數量相當於標籤列排列組合的個數,當標籤列有1、2 k 的時候,這基本就是不可能的。通常的做法是在數倉提前將熱點的組合聚合過濾成新欄位,或者只對熱點組合加索引,但這樣都很不靈活。
通過採用bitmap的辦法,按欄位重組成Bitmap。
標籤 | 標籤值 | bitmap欄位底層二進位制結構 |
---|---|---|
city_id(城市id) | 1001 | 00000110 |
city_id(城市id) | 1002 | 00011000 |
city_id(城市id) | 1003 | 00100000 |
is_user_start(是否啟動) | 1 | 00011110 |
is_user_start(是否啟動) | 0 | 00100000 |
is_evl(是否估價) | 1 | 00001110 |
is_evl(是否估價) | 0 | 00110000 |
is_order(是否下單) | 1 | 00001010 |
is_order(是否下單) | 0 | 00110100 |
把資料調整成這樣的結構,再進行條件組合,那就簡單了。比如: 1001城市 + 下單 = Bitmap[00000110] & Bitmap[00001010]= 1 ,這個計算速度相比寬表條件篩選是快很多的,如果資料是比較稠密的,bitmap可以極大的節省底層儲存,如果資料比較稀疏,可以採用RoaringBitmap來優化。
在支援貨拉拉A/B實驗平臺業務的場景中,會有一個實驗對應很多司機的情況,因為要在數倉處理成明細寬表,來支援OLAP引擎的使用,隨著維度的增多,資料發散的情況變得很嚴重,數倉及OLAP的儲存與計算資源都消耗巨大。為了解決這個痛點,在架構組同事建議下,引入了BitMap,將組裝好的司機id陣列轉換成RoaringBitmap格式,再傳入到OLAP引擎裡面使用。
在數倉應用層,由於引入了BitMap,重構了原本的資料表結構及鏈路,並優化了數倉的分層。不僅讓整個鏈路耗時縮短了2個多小時,還節省了後續往OLAP引擎導數的時間,再算上數倉層的計算與儲存資源的節省,很完美的實現了降本增效!而在OLAP引擎層,由架構組的同事通過二次開發,支援了Bitmap的使用,也取得了很不錯的效果。
具體的落地與應用則在下篇文章給大家詳細分享。
本文首先通過對於BitMap的簡單實現以及對於Java中BitSet原始碼的分析,提升讀者對於其底層原理的理解,然後分析了BitMap的特點,並針對其儲存優化的方案,講解了RoaringBitmap技術的原理,最後列舉了對於BitMap的常見使用場景。希望大家讀後都能有所收穫。
更多關於貨拉拉BitMap巨量資料的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45