Hbase 行键设计(rowkey) 实现多条件查询

转自:http://blog.csdn.net/alphags/article/details/53786777


摘要

本文主要内容是通过合理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
  • 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
  • 1
  • 2
  • 3
  • 4
  • 5

以上5个数我们用来表示hbase中每一行的行键,例如我们要找到,从左数第2个字节是 
11010101 
的数据,那我们应该怎样计算呢? 
我们需要第一个比较器可以实现以下步骤。

  1. 转化匹配条件:我们要找第2个字节是11010101 的行,我们可以构建一个用于查找的值,如下
00000000 11010101 00000000 00000000
  • 1
  • 1
  1. 构建模板数字:此处模板数字用于与行键进行与操作,以过滤其他值对比较的干扰。
00000000 11111111 00000000 00000000
  • 1
  • 1

3、读取一行rowkey 与第一步中我们的模板进行与操作,例如我们现在读取到第一个rowkey

10100111 11000101 10110100 01100011
  • 1
  • 1

与模板数字进行与操作后结果为

00000000 11000101 00000000 00000000
  • 1
  • 1

4、得到最终结果 。我们可以看到,在第3步最后得到的结果,如果与我们的查询条件相等的话返回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
  • 1

2、过滤模板数字

00000000 11111111 00000000 00000000
  • 1
  • 1

3、读入行键并与模板数字进行与操作,以第一行为例得到的结果是

00000000 11000101 00000000 00000000
  • 1
  • 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个字节,根据实际需求进行调整。


你可能感兴趣的:(大数据)