映射的存储模型
http://blog.sina.com.cn/s/blog_693f08470101ovfp.html
http://blog.sina.com.cn/s/blog_693f08470101omuq.html
映射的存儲模型?–?面向列的存儲和行存儲
?
1個月沒跟大家見面了,這個月真的是很累,做了很多事,天天加班,不過結(jié)還都不錯,以后調(diào)休吧?~
雙11不知道各位感覺如何?是不是覺得很平穩(wěn)呢??在這次雙11?里面,中間件團(tuán)隊配合業(yè)務(wù)團(tuán)隊使用了一種全新的全鏈路壓測方式進(jìn)行了線上性能驗證,提前就預(yù)演了所有可能的壓力,因此這次雙?11是我五年雙11?支持過程中最淡定的一次?~
?
然后,又使用了阿里中間件的全部技術(shù)支持了某大型公司的企業(yè)的業(yè)務(wù)邏輯重構(gòu)。讓他們的系統(tǒng)業(yè)務(wù)架構(gòu)有了一次全新的變化,讓他們的系統(tǒng)具備了自由擴(kuò)展的能力,從此不再擔(dān)心系統(tǒng)無法支持用戶增長了。順便也就驗證了我們解決問題的整體思路是一個可復(fù)制的思路,將會在未來有更廣闊的發(fā)展空間?–?玩法變了啊,各位感受到了么?
?
好,話題回到模型上,今天我們開啟一個新的主題,就是映射的存儲模型。換句話說,在了解了?k-v的基本機(jī)制之后,下一步我們研究一下我們的數(shù)據(jù)應(yīng)該如何使用映射。
回顧一下映射,映射的本質(zhì)就是一個?map,給出map?的key,?map就返回key?所對應(yīng)的?value。我們也介紹過,在計算機(jī)里的大部分事情其實都可以被表示為一個?map,比如,給定扇區(qū)號,返回給定扇區(qū)的數(shù)據(jù)等等。
?
我們已經(jīng)知道了?Map,而在這里我們最需要解決的問題就是,給定一組數(shù)據(jù),應(yīng)該如何存放到我們的映射里呢?其實存放的方法非常多樣,而核心的問題是用戶的實際需求。
?
這里我們需要舉個例子,假定目前我們需要存儲的數(shù)據(jù)是一組數(shù)據(jù)。
| bizOrderID | sellerID | buyerId | content |
| 0 | 0 | 4 | ‘a(chǎn)’ |
| 1 | 0 | 3 | ‘c’ |
| 2 | 0 | 2 | ‘I’ |
| 3 | 0 | 1 | ‘d’ |
| 4 | 3 | 4 | ‘b’ |
| 5 | 2 | 4 | ‘f’ |
?
?
針對這樣一組數(shù)據(jù),我們其實是有很多種不同的存儲方式的。
?
第一種,以pk作為key,以其他數(shù)據(jù)作為value?
| Map | |
| Key | Value |
| {bizOrderId:0} | {sellerID:0,buyerId:4,content:’a’} |
| {bizOrderId:1} | {sellerID:0,buyerId:3,content:’c’} |
| {bizOrderId:2} | {sellerID:0,buyerId:2,content:’i’} |
| {bizOrderId:3} | {sellerID:0,buyerId:1,content:’d’} |
| {bizOrderId:4} | {sellerID:3,buyerId:4,content:’b’} |
| {bizOrderId:5} | {sellerID:2,buyerId:4,content:’f’} |
?
可以看出,在這種存儲的實現(xiàn)中,每一行的數(shù)據(jù)都冗余了列的名字和該列所對應(yīng)的類型信息。
這種方式有個專用的名詞,就叫”面向列的存儲“,當(dāng)然,雖然出現(xiàn)了“列”字,但各位可千萬不要望文生義,這面向列的存儲,跟我們后面要看到的列存基本不沾邊。。
這種存儲方式,最大的好處就是每個value里面的數(shù)據(jù)可以完全自己定義,目前主流的實現(xiàn)是使用json來存儲value數(shù)據(jù),這樣,如果業(yè)務(wù)要求增加一個列,那就在json拼裝的時候額外增加一個列就行了。而如果業(yè)務(wù)需要減少一個列,也可以直接在代碼里拼裝,減少了運維的成本。
不過這樣做也不是完全沒有代價,額外的冗余數(shù)據(jù)就意味著額外的空間消耗,所以目前通用的優(yōu)化方案,利用了列的個數(shù)本身是比較有限的,這個特性,于是利用一個新的map_bit. Key為列的名字,value為一個bit .?從而減少冗余數(shù)據(jù)帶來的過多空間消耗。如下:
| Map_bit | |
| Key | Value(bit) |
| sellerID | ‘a(chǎn)’ |
| bizOrderId | ‘b’ |
| buyerId | ‘c’ |
| content | ‘d’ |
?
| Map | |
| Key | Value |
| {b:0} | {a:0,c:4,d:’a’} |
| {b:1} | {a:0,c:3,d:’c’} |
| {b:2} | {a:0,c:2,d:’i’} |
| {b:3} | {a:0,c:1,d:’d’} |
| {b:4} | {a:3,c:4,d:’b’} |
| {b:5} | {a:2,c:4,d:’f’} |
?
有了這么個結(jié)構(gòu),面向列的存儲里面數(shù)據(jù)冗余的大問題得到了部分的解決,然而這種模式還有優(yōu)化的空間。在大部分情況下,我們的業(yè)務(wù)模型基本上是穩(wěn)定的,不會過于頻繁的發(fā)生變化。如果我們能夠有這樣的假定,那么我們就可以將列所對應(yīng)的位置固定下來,用一個叫map_schema的表格來存儲,不就不需要冗余上面的那些’a’,’b’,’c’,’d’等bit信息了。可以更多地節(jié)省存儲空間啊。
| map_schema | |
| Key | Value |
| sellerID | 表格內(nèi)的位置:1 |
| bizOrderId | 表格內(nèi)的位置:0 |
| buyerId | 表格內(nèi)的位置:2 |
| content | 表格內(nèi)的位置:3 |
?
| Map | |||
| 0 | 0 | 4 | ‘a(chǎn)’ |
| 1 | 0 | 3 | ‘c’ |
| 2 | 0 | 2 | ‘I’ |
| 3 | 0 | 1 | ‘d’ |
| 4 | 3 | 4 | ‘b’ |
| 5 | 2 | 4 | ‘f’ |
?
這種方式就是我們最常見的”行存“了,這種方式的比較于面向列的存儲,最主要的優(yōu)勢就是空間消耗更少,而且申請空間的效率更高,因為用戶在開始的時候就已經(jīng)指定了所有數(shù)據(jù)所需要的數(shù)據(jù)類型(空間消耗),因此消耗的空間是相對比較固定的。
而對于面向列的存儲而言,由于不能提前假定每一行數(shù)據(jù)的大小,所以只能使用變長數(shù)據(jù)存儲格式來存儲數(shù)據(jù)。?因此也會有更多的空間浪費,并且提高申請存儲空間的代價。
無論是面向列的存儲,還是行存,他們的寫入都可以只通過一次map.put(key,value)就可以完成。所以寫入的效率比較高,如果按照pk這一列做查詢,速度也會非常快,因為也只需要一個map.get(key)就可以取出需要的數(shù)據(jù),速度自然也就是很快的。
上上周我們討論了行存儲,那么現(xiàn)在呢我們來看看列存的存儲方式
?
在一個文件塊內(nèi)是能夠放多個數(shù)據(jù)的,行存的存儲方式是一個文件塊內(nèi)放置多行的數(shù)據(jù)。而列存,顧名思義,就是一個文件塊內(nèi)放置多列的數(shù)據(jù)了唄~~
?
舉個例子:
對上面的那張表,如果我們按照列的方式存儲到映射里,應(yīng)該如何存儲呢?
首先,我們需要一個映射map_columns來存儲每個列的具體映射表是哪個。
?
?
| Map_columns | |
| Key | Value(bit) |
| sellerID | Map_sellerID |
| bizOrderID | Map_bizOrderID |
| buyerID | Map_buyerID |
| content | Map_content |
?
第一組數(shù)據(jù),用于存儲sellerID的映射關(guān)系。當(dāng)然真實存儲的時候,不需要去存儲key是bizOrderID這個信息的,因為這是所有列存所必須遵守的,而對于value而言,Map的名字其實就標(biāo)記了這組映射的value值的所有信息,所以也不需要記錄的,這里用()標(biāo)記出來,主要是為了方便理解
| Map_sellerID | |
| Key(bizOrderID) | Value(sellerID) |
| 0 | 0 |
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
| 4 | 3 |
?
第二組數(shù)據(jù)(buyerID),同樣的,數(shù)據(jù)的類型信息在Map_columns里面就記錄好了。這里直接記錄真正的數(shù)據(jù)就行了。
| Map_buyerID | |
| Key(bizOrderID) | Value(buyerID) |
| 0 | 4 |
| 1 | 3 |
| 2 | 2 |
| 3 | 1 |
| 4 | 4 |
| 5 | 4 |
?
第三組數(shù)據(jù)(content)
| Map_content | |
| Key(bizOrderID) | Value(buyerID) |
| 0 | ‘a(chǎn)’ |
| 1 | ‘c’ |
| 2 | ‘I’ |
| 3 | ‘d’ |
| 4 | ‘b’ |
| 5 | ‘f’ |
?
?
在介紹了列存這種數(shù)據(jù)結(jié)構(gòu)之后,我們來與行存做一個比較分析,來去看看為什么列存和行存會有完全不同的一些技術(shù)特性。
?
數(shù)據(jù)結(jié)構(gòu)這種東西,沒有一種能夠解決所有的問題,更多地時候是在各種權(quán)衡和妥協(xié)中找到一個平衡點的過程,對于行存和列存,這東西也是一樣的。。
?
古人云~任何不帶使用場景討論的數(shù)據(jù)結(jié)構(gòu)優(yōu)缺點分析都是耍流氓。
我們需要先看看用戶怎么用我們的存儲的。
應(yīng)用總是希望快速的完成數(shù)據(jù)的寫入得,而用戶一般來說一定是按照整行的方式進(jìn)行數(shù)據(jù)的寫入,因為這是用戶正常組織數(shù)據(jù)的方式嘛。。。
?
空間分析:
?????????對行存儲來說,壓縮一般是按照塊來進(jìn)行的,常見的壓縮方式的核心思想其實也并不復(fù)雜。
?????????第一個思路是字典壓縮,也就是將固定的bytes序列換成更小的一個bit來表示,這樣就可以用更小的空間來存儲更多地內(nèi)容了,比如人類在這世界上只有三種性別,所以字典只需要2個bit(存儲四個type),所以就算人類人口成長到了100億,每一個人還是只需要兩個bit就可以存儲嘛。
?????????第二個思路就是bitMap(目前很熱的bloomFilter其實也是bitmap思想的一個特例),所謂bitMap,是利用bit數(shù)組的下標(biāo)的true(1)/false(0)來標(biāo)記某個數(shù)字是否已經(jīng)存入到這個數(shù)組內(nèi)。
比如,要存儲0,3,5,8四個數(shù)字,所有數(shù)字的最大值是8.那么我們可以建立一個bitMap來存儲這組數(shù)字:
| true(1)/false(0) | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 |
| 位置 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
?
這樣,我們只需要一個bit數(shù)組就可以存儲原來需要多個bytes才能存儲的多組數(shù)字了。
?
?
基本上來說,壓縮的核心思路就是有機(jī)的結(jié)合上面這兩個元素,結(jié)合各種映射關(guān)系來完成數(shù)據(jù)壓縮的。
在這個壓縮核心思想的基礎(chǔ)上,還有很多擴(kuò)展的方法,最主要的方式就是把bitMap和Btree結(jié)合進(jìn)行壓縮,bitMap用來表示某個數(shù)據(jù)在整個數(shù)組內(nèi)的重復(fù)出現(xiàn)的具體位置是哪些,又因為有多個數(shù)據(jù),所以在bitMap上面再套一個btree,用來幫助尋找某個key是否存在,以及如果存在的話,具體位置有哪些。
?
可以發(fā)現(xiàn),壓縮比率很高的場景,一般來說都是數(shù)值類型的東西。所以如果你把一大組數(shù)值類的數(shù)據(jù)放在一起,并且這個數(shù)值類的數(shù)據(jù)重復(fù)度很高,或者比較有規(guī)律,那么做壓縮的效果一般來說就會很好。
?
然而,對于行存儲來說—就以我們的一行數(shù)據(jù)為例:
| bizOrderID | sellerID | buyerId | content |
| 0 | 0 | 4 | ‘a(chǎn)’ |
這行數(shù)據(jù)里又出現(xiàn)了數(shù)字,又有字符,那么這種場景下就很難選擇完全使用數(shù)值類壓縮方式來做,而只能選擇字典壓縮了,很容易想象,字典壓縮因為是通用壓縮法,所以壓縮比率也就比較一般了。。
而對于行存儲而言,這方面就能獲得很大的好處,因為數(shù)值類的數(shù)據(jù)被物理的放在一起了,而且性別啊,門牌號啊,電話號碼啊這類明顯有一定規(guī)律的數(shù)據(jù)被物理上的放在了一起,所以壓縮效果好那就是自然而然的事情咯~
?
一般來說,行存儲的壓縮比率大概是1:1.5~2左右頂頭了。?也就是能減少一倍的數(shù)據(jù)量。而對于列存儲而言,大部分情況下列存儲的壓縮都能在1:20 ~ 1:30?以上,極大的減少了空間消耗。
空間消耗的減少意味著什么?原來必須存在磁盤的“大數(shù)據(jù)”現(xiàn)在可以用更少的機(jī)器,甚至是單機(jī)就可以搞定,節(jié)省機(jī)器成本,同時還能提升一個重要的指標(biāo),我們下面就立刻分析一下。
?
寫入效率分析:
?????????對于行存儲來說,一行數(shù)據(jù)的寫入就對應(yīng)了一個map.put操作,所以效率自然是很高。
?????????而對于列存儲來說,一行數(shù)據(jù)的寫入,假設(shè)這個數(shù)據(jù)列有4列,那么就會對應(yīng)4個map.put操作,如果有6列,那么就對應(yīng)6個map.put操作。效率低于列存。
?
?????????然而,這并不是絕對的,讓我們把條件稍微做個變換,效果就會立刻不同,如果用戶不需要單次寫入后立刻返回,而是可以一次寫入100條以上的數(shù)據(jù),那么形勢就會立刻發(fā)生逆轉(zhuǎn)!
?????????因為列存是將壓縮后的數(shù)據(jù)寫入磁盤的,聯(lián)想壓縮比率,原來需要寫20mb的數(shù)據(jù),現(xiàn)在可能只寫1mb,與cpu和內(nèi)存相比,磁盤目前仍然是整個系統(tǒng)的核心瓶頸,而且差距很大,因此減少硬盤寫入就直接的減少了寫入一組數(shù)據(jù)所需要的時間。
?????????因此,列存在數(shù)據(jù)導(dǎo)入的場景下?lián)碛羞h(yuǎn)遠(yuǎn)快于行存儲的數(shù)據(jù)寫入速度也就不足為奇了。
?
那么,想不想知道行存,列存是如何解決各類用戶奇葩的多維度查詢需求的??下一篇我們將進(jìn)入這一領(lǐng)域進(jìn)行探索。
總結(jié)
- 上一篇: 编写你的第一个垃圾收集器
- 下一篇: 利用二级指针删除单向链表