學習--Hbase 行鍵設計(rowkey) 實現多條件查詢

HBASE的使用跟業務邏輯有很強的關聯性,就像本文裏提到的例子使用ElasticSearch更合適。HBASE適合那種使用key-value模式的快速查詢,多字段查詢還是不適合它。 
所以大家如果看本文的話,就全當是加深對hbase過濾器的理解吧,內容概括起來就是實現一個使用位運算的比較器。

摘要
本文主要內容是通過合理hbase 行鍵(rowkey)設計實現快速的多條件查詢,所採用的方法將所有要用於查詢中的列經過一些處理後存儲在rowkey中,查詢時通過rowkey進行查詢,提高rowkey的利用率,加快查詢速度。行鍵(rowkey)並不是簡單的把所有要查詢的列的值直接拼接起來,而是將各個列的數據轉成整型(int)數據來存儲。之後實現兩個自定義的比較器(comparator):一個是相等比較器,用於實現類似於SQL的多條件精確查找功能。 
select * from table where col1='a' and col2='b'

另一個是範圍比較器,用於實現類似於SQL語句 
select * from table where col3 > '10' and col4<'100' 
這樣的範圍查找功能。 
當兩個比較器配合使用再結合hbase的過濾器,以實現類似於下面這條SQL語句這樣多條件的查詢 
select * from table where col1='a' and col2='b' andcol3 > '10' and col4<'100' 
文章源碼位於https://github.com/alphg/hbase.rowkeycomparator

問題背景
hbase 作爲開源列式存儲,使用起來與傳統的關係型數據庫還是有很多不同的。就以我所在公司爲例,下面的數據是一些網頁連通性的數據,

{ "_id" : { "$oid" : "584a6e030cf29ba18da2fcd5"} , "url" : "http://www.nmlc.gov.cn/zsyz.htm" , "md5url" : "ea67a96f233d6fcfd7cabc9a6a389283" , "status" : -1 , "code" : 404 , "stime" : 1481272834722 , "sdate" : 20161209 , "sitecode" : "1509250008" , "ip" : "10.168.106.153" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272835222} , "free" : 0 , "close" : 0 , "queue" : 1 , "scantype" : 1 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e020cf224463e76c162"} , "url" : "http://www.xzxzzx.gov.cn:8000/wbsprj/indexlogin.do" , "md5url" : "fd38c0fb8f6e839be56b67c69ad2baa5" , "status" : -1 , "code" : 503 , "stime" : 1481272828174 , "sdate" : 20161209 , "sitecode" : "3203000002" , "ip" : "10.117.8.89" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272834887} , "free" : 0 , "close" : 0 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e020cf27d1a31f617e0"} , "url" : "http://www.nmds.gov.cn/portal/bsfw/nsfd/list_1.shtml" , "md5url" : "d51abcd8edff79d23ca4a9a0576a1996" , "status" : -1 , "code" : 404 , "stime" : 1481272822971 , "sdate" : 20161209 , "sitecode" : "15BM010001" , "ip" : "10.162.86.176" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272834846} , "free" : 0 , "close" : 0 , "queue" : 0 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e020cf29ba18da2fcd4"} , "url" : "http://beijing.customs.gov.cn/publish/portal159/tab60561/" , "md5url" : "e27bbc9192e760bacc23c226ffd90219" , "status" : -1 , "code" : 503 , "stime" : 1481272832559 , "sdate" : 20161209 , "sitecode" : "bm28020001" , "ip" : "10.168.106.153" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272834766} , "free" : 0 , "close" : 0 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e020cf29ba18da2fcd3"} , "url" : "http://www.nss184.com/web2/newlist_index.aspx?classid=1" , "md5url" : "cbc2c0571464621024c89aa019cd09ef" , "status" : -1 , "code" : 404 , "stime" : 1481272826788 , "sdate" : 20161210 , "sitecode" : "BT10000001" , "ip" : "10.168.106.153" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272834732} , "free" : 0 , "close" : 1 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e020cf2847bb13af52c"} , "url" : "http://cgw.bjdch.gov.cn/n1569/n4860273/n9719314/index.html" , "md5url" : "00a18048ed95f1c057fccc8928ddf610" , "status" : -1 , "code" : 503 , "stime" : 1481272803601 , "sdate" : 20161208 , "sitecode" : "1101010059" , "ip" : "10.117.187.7" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272834150} , "free" : 1 , "close" : 0 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e020cf29ba18da2fcd2"} , "url" : "http://www.qdn.gov.cn/zwdt/ztfw/shbzfw.htm" , "md5url" : "e6bfa0a07e773e3bab27a37f36ff221a" , "status" : -1 , "code" : 404 , "stime" : 1481272833479 , "sdate" : 20161209 , "sitecode" : "5226000038" , "ip" : "10.168.106.153" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272834046} , "free" : 0 , "close" : 0 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e010cf29ba18da2fcd1"} , "url" : "http://www.caac.gov.cn/E1/E2/" , "md5url" : "e6217482388cbc57aa80422c3f64bb35" , "status" : -1 , "code" : 404 , "stime" : 1481272833297 , "sdate" : 20161209 , "sitecode" : "bm70000001" , "ip" : "10.168.106.153" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272833723} , "free" : 0 , "close" : 0 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e010cf22c906fb6f846"} , "url" : "http://www.ny.xwie.com/Thought/" , "md5url" : "b7912f3bdb50be7b58f5a67d65273201" , "status" : -1 , "code" : 404 , "stime" : 1481272821713 , "sdate" : 20161209 , "sitecode" : "4408250003" , "ip" : "10.168.156.196" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272833498} , "free" : 0 , "close" : 0 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
{ "_id" : { "$oid" : "584a6e010cf29ba18da2fcd0"} , "url" : "http://www.guoluo.gov.cn/html/1746/List.html" , "md5url" : "e353cd577fd721eb71538d0938d041f7" , "status" : -1 , "code" : 404 , "stime" : 1481272832723 , "sdate" : 20161209 , "sitecode" : "6326000004" , "ip" : "10.168.106.153" , "port" : 5200 , "type" : 2 , "intime" : { "$date" : 1481272833472} , "free" : 0 , "close" : 0 , "queue" : 1 , "scantype" : 0 , "scanmemo" : ""}
1
2
3
4
5
6
7
8
9
10
11
每行json字符串都表示某一條網址的連通性掃描信息,部分需要檢索的json屬性的意義如下

md5url url的md5的值
status 掃描狀態
code http訪問返回碼
sdate 掃描日期
sitecode 所屬站點
type 掃描類型
free 是否收費
close 是否關閉
queue 等待隊列
scantype 掃描類型 
其實我們不需要知道其每個屬性都代表什麼意思,我們只需要知道我們在查詢的時候有可能會用到上述一個或多個屬性,例如 
查詢某一條URL在某一天的掃描數據 SQL表示 
select * from table where md5url='somemd5url' and sdate='somedate' 
或者是我們要查詢某一天連不通的網址(返回碼是404) 
select * from table where sdate='somedate' and code='404' 
或者查詢某個URL在過去某幾天內的數據 
select * from table where sdate<'enddate' and sdate>'startdate' and md5url='somemd5url' 
以上只是簡單列舉幾種查詢的需求,實際生產中會有更多種累的查詢需求,那如何設計hbase 表結構就成爲解決問題的關鍵。
行鍵(rowkey)設計
根據問題需求,我們看到需要查詢的屬性多達10個,如果說查詢每一個屬性的值時都使用columnvaluefilter 的話那速度是相當慢的,hbase 需要去進行全表的掃描’滿山遍野’地找與你值相匹配的行,而且使用columnvaluefileter也很難實現一次查找中放入多個找找條件(PS:也許是我理解不夠深入啊,不懂如何使用columnvaluefileter 通過一次scan可以得到多列都滿足要求的數據,這裏暫且不談。)。既然全表掃描的這種方式被否定,那怎麼樣解決這個問題?關鍵就在於行鍵(rowkey) 
rowkey設計有以下幾個原則(http://www.cnblogs.com/kxdblog/p/4328699.html) 
1、長度越短越好 
2、唯一性 
3、散列性 
我們如何能所儘可能多的屬性值以儘可能短的形式放到rowkey就是解決問題的關鍵(長度儘可能短滿足了第一個原則,儘可能多的屬性放到rowkey中,如果兩行數據不同那麼其形成的rowkey也應該是不同的這就滿足了第二個原則,第三個原則我們暫且不談) 
回頭看我們的數據,我們發現每個屬性的值的類型有三種:字符串(String)類型,如md5url、siteCode;整型(int),如code、type;布爾型(boolean),如free ,那我們應該怎麼樣把這三種類型的數據融和到行鍵當中呢?

String 類型 :我們知道對於一個字符串(String)類型的數據根據其字符集編碼以及字符數量的不同其佔用的字節數是不固定的,可是我們知道字符串的哈希值是一個Int型的數據,Int在java內存中固定是佔4個字節。所以說我們如果在組合行鍵時並不直接使用String而是使用其所對就的哈希值就可以縮短其所佔的字節數。 
Int 類型:我們儘量讓數據以Int類型去組成rowkey,所以int 不需要處理。 
布爾類型 : 我們知道布爾類型使用一位1/0 表示真/假。那布爾類型可以比Int 更短(int 佔4個字節32位),但此處爲了簡單統一布爾類型也一併按照Int來處理(有興趣的朋友可以自己實現)

我們在確定了數據以何種形式放入rowkey後,我們還需要確定各個屬性值在rowkey中的先後順序,這個我們後邊再說,這裏我們就以下邊所示的順序組成rowkey 
md5url、siteCode、status、code、sdate、type、free、close、gueue、scantype

至此我們已經確定瞭如何生成rowkey

算法
在確定了rowkey的生成方式後,如何查找rowkey就成爲了現在的問題。 
hbase給我們提供了多種過濾器和多種比較器(可以查看《hbase權威指南》中關於過濾器的描述),每種過濾器都有其特點,其中BitComparator 可了我一定的啓示,我們可不可以也使用這種位的方式來進行匹配呢? 
我們以一個簡單的示例來說明查找行鍵的算法,一個int 型數據是佔四個字節。現在我們有多個int型數據

10100111 11000101 10110100 01100011   
10000101 11100001 00110000 00101011   
10100011 11010101 10111100 01101011   
00100111 11000111 10110101 01110011   
00110111 11010101 10100100 01101011   
1
2
3
4
5
以上5個數我們用來表示hbase中每一行的行鍵,例如我們要找到,從左數第2個字節是 
11010101 
的數據,那我們應該怎樣計算呢? 
我們需要第一個比較器可以實現以下步驟。

轉化匹配條件:我們要找第2個字節是11010101 的行,我們可以構建一個用於查找的值,如下
00000000 11010101 00000000 00000000
1
構建模板數字:此處模板數字用於與行鍵進行與操作,以過濾其他值對比較的干擾。
00000000 11111111 00000000 00000000
1
3、讀取一行rowkey 與第一步中我們的模板進行與操作,例如我們現在讀取到第一個rowkey

10100111 11000101 10110100 01100011
1
與模板數字進行與操作後結果爲

00000000 11000101 00000000 00000000
1
4、得到最終結果 。我們可以看到,在第3步最後得到的結果,如果與我們的查詢條件相等的話使compareTo方法的返回值爲0,如果不相等則返回1 表示不滿足條件。

根據這個原理,同時匹配多個字節相等也是可行的。例如我們要找第一個字節是10000101第四個字節是00101011的行,那查詢數字便可以是10000101 00000000 00000000 00101011 過濾模板數字應該是11111111 00000000 00000000 11111111 
然後進行上述步驟就可以正常得到滿足要求的行。

以上算法解決了select * from table where c1='v1' and c2='v2' 這種精確匹配的查詢要求,但是對於範圍查詢還是沒有解決,我們需要另外一個比較器來解決這個問題。

hbase 當中呢已經給我們提供了比較運算符了,我們需要解決是如何運算比較大小的問題。

仍然使用上述數字爲例,我們要找第2個字節大於11010101 的行。

1、仍然是構建查詢條件

00000000 11010101 00000000 00000000
1
2、過濾模板數字

00000000 11111111 00000000 00000000
1
3、讀入行鍵並與模板數字進行與操作,以第一行爲例得到的結果是

00000000 11000101 00000000 00000000
1
4、最後返回第3步中的結果與查詢條件的差值。

我們可以看到上邊提到的兩個比較器操作過程大體相同,只有到了第4步不同,那爲什麼我們不能結合到一起呢? 
這是因爲比較大小與比較相等是不同的,因爲hbase過濾器中的比較運算符我們一次只能傳一個(或者大於或者小於)。所以你不能在一次比較中即找到第2個字節大於指定值,第3個字節小於指定指。所以這個比較器正確的用法應該是。在一次掃描中構建多個過濾器,每一個過濾器中的比較器僅比較行鍵中的某一個字節,將多個過濾器結合使用就可以在一次掃描中查出滿足多個範圍條件的行。例如,我要找第2個字節大於11000101 同時第3個字節小於10111100的值時,我們需要兩個過濾器第一個傳入00000000 11000101 00000000 00000000 同時傳入相應的比較運算符,第2個比較器中傳入0000000 00000000 10111100 0000000 和相應的操作符,然後將這兩個過濾器同時加到掃描器中,以在一次掃描中得到滿足多個條件的數據 。

將上述例子擴展到我們本文開頭的問題中大體也是一樣的,僅僅一些小細節上會有出入,例如每次我們查詢的時候不再是要求某一個字節是某個值,而是要求在某一位置的整數滿足某個值。

代碼實現
本文中所用到的代碼可以在https://github.com/alphg/hbase.rowkeycomparator 得到,我覺得在明白原理後,大家完全可以按照自己的編程習慣去實現,我的代碼僅作爲參考(而且我覺得我寫的也不好)。這裏我羅列一些細節和注意事項: 
1、在原理中所說兩個比較器需要進行序列化,這裏使用到了google的protobuf 不會的同學可以簡單而度一下,非常方便。 
2、爲了減少比較器序列化後的長度,也減少序列化與反序列化部分代碼,同時方便使用,算法中所提到的過濾模板是不做序列化的,而是在構造函數中,根據查詢條件進行創建,細心的同學也能從示例當中看出其與查詢條件的對應關係,從而在使用時進行創建。 
創建過濾模板這個操作沒有放到compare函數中是爲了提高效率,在hbase中每個比較器是複用的,所以僅需要在構造函數中計算一次,每次比較時都可以重複利用。 
3、第三點是很重要的一點。 
在處理行鍵(rowkey)的過程中,我代碼中一直都使用的是類型byte[] (字節數組 )除了print外沒有一處會將其轉化爲string,這是因爲轉string會有很多不確定性因素,例如,如果你的字節數組中有某一個字節值是0 ,那這個string會不會因爲這個0而提前被截斷?還有string的編碼問題,總之感覺轉string問題多多,我們的算法也不需要其按照string來進行處理,SO,不要將行鍵轉成String 
4、文章中我們將字符串類型的值轉換成其hashcode 存在了rowkey中,所以對其進行相等或不相等的比較是沒有問題的,但是如果用於比較大小是會有問題的,如 字符串‘def’的字典順序是要大於’abcdefghjk’的,但是其hashcode是否仍然大於呢?顯然這是不確定的。so 應在上層實際時避免比較字符串大小。 
5、我的樣例代碼中有一些logger 用來打印一些日誌,以驗證某些想法,不需要大家可以將其去掉。

測試以及部分問題解決
我在完成上述代碼之後進行了一些測試,功能上是滿足要求的,但是在部分性能上還是有些問題。 
公司內網環境,3臺普通服務器(這裏就不報配置了)上測試,1000萬數據量,使用get進行精確查找,查詢一次需要300ms左右 ,速度還可以接受。 
但是使用scan,結合本文中的內容進行掃描時,不作任何其他限制的掃描時其速度那是相當穩定的慢 50秒左右。 
這是因爲每次掃描都對行鍵(rowkey)進行一次全表掃描,這樣的話速度慢就可以理解了,那怎麼樣解決這個問題呢? 
就是要限定掃描行鍵的起止範圍。這樣可以大幅度減少掃描的行數從而提高響應時間。(這部分內容就不細說了,大家可以看相關書籍瞭解如何指定起始和結束行鍵) 
舉個例子,我要找md5url=’00a18048ed95f1c057fccc8928ddf610’ 我們可以指定 
起始行鍵第一個整數是’00a18048ed95f1c057fccc8928ddf610’.hashcode() 後邊所有的值都是0 
結束行鍵第一個整數是’00a18048ed95f1c057fccc8928ddf610’.hashcode() +1 後邊同樣都是0 
這樣子的話,掃描僅僅rowkey 以值’00a18048ed95f1c057fccc8928ddf610’.hashcode() 開頭的數據,從而減少掃描量。

說到這裏之後就跟我們前邊所說rowkey中各個屬性的先後順序有了呼應,本文中內容以md5url開頭,這樣當我們查詢條件中有md5url時我們可以利用其很輕鬆的指定起始行鍵,但是如果查詢條件中包含md5url呢?那查詢又要回到原始的全表掃描了。 
這時候我們就需要用到hbase的二級索引,這個時候就要根據我們的查詢需求,調整各個屬性的先後位置,構建索引表。這個從書《HBase企業應用開發實戰》的實例中可以找到方法,有時間的話我也會再以後的文章中寫一些這方面的內容。

最後
其實在處理公司這個任務的時候真是感覺有很多東西要寫,但真正寫出來卻沒多少東西,感覺自己漏了不少東西,也不知道有沒有寫清楚,如果有什麼寫的不清楚的地方,或者有什麼問題大有可以在評論裏提出來。 
這種行鍵的設計方法其實還有很多可以改進的地方,例如布爾類型的數字我們使用一位來表示(而不是文章中的一個整型)等等 ,大家在工作中根據需要再進行改進,本文就不再做深入的研究了。

注意本文中爲了描述簡單使用了String的Hashcode,但是我們知道當數據量很大時,hashcode有可能出現重複的情況,即不同的URL轉化成的hash值是相同的,爲了避免這種問題(或者說是儘量減少這種情況發生的概率)我們可以使用string的md5值,如果使用16位的md5加密其所對應的值佔8個字節,如果使用32位數值佔16個字節,根據實際需求進行調整。

原文:https://blog.csdn.net/alphags/article/details/53786777 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章