2012年因为业务需求,我们的底层数据库从Mysql迁移到HBase上面,正好也亲身经历了HBase-Client从0.92到0.94变化。我们总结了一些业务上面使用HBase的办法,希望本文能够对业务上面刚刚使用HBase的人一些帮助,降低入门门槛。
构建单例的HBaseFactory,我们只需要关心三个事情
我们使用的是HTablePool构建一个HBaseFactory对象
HTablePool您可以看成JDBC的连接池,适合多线程使用环境,如果需要把连接“还”给连接池的话,只需要调用HTableInterface#close() 就可以了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
public
interface
HBaseFactory
{
/**
* 通过 tableName 来获取这个 Table
*/
HTableInterface
getHTable
(
String
tableName
)
;
/**
* 关闭某个table
*/
void
closeHTable
(
HTableInterface
hTableInterface
)
;
/** only for unit test*/
boolean
deleteTable
(
String
tableName
)
;
/** only for unit test*/
HTableDescriptor
createTable
(
String
tableName
,
int
maxVersion
)
;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
public
class
HBaseFactoryImpl
implements
HBaseFactory
{
static
Logger
logger
=
LoggerFactory
.
getLogger
(
HBaseFactoryImpl
.
class
)
;
private
HTablePool
hTablePool
=
null
;
private
HBaseAdmin
hBaseAdmin
=
null
;
@
Inject
public
HBaseFactoryImpl
(
String
quorum
,
String
parent
,
int
maxSize
)
{
checkArgument
(
isNotBlank
(
quorum
)
)
;
checkArgument
(
isNotBlank
(
parent
)
)
;
Configuration
conf
=
HBaseConfiguration
.
create
(
)
;
conf
.
set
(
"hbase.zookeeper.quorum"
,
quorum
)
;
conf
.
set
(
"zookeeper.znode.parent"
,
parent
)
;
conf
.
set
(
"hbase.client.retries.number"
,
"5"
)
;
conf
.
set
(
"hbase.client.pause"
,
"200"
)
;
conf
.
set
(
"ipc.ping.interval"
,
"3000"
)
;
conf
.
setBoolean
(
"hbase.ipc.client.tcpnodelay"
,
true
)
;
hTablePool
=
new
HTablePool
(
conf
,
maxSize
)
;
try
{
hBaseAdmin
=
new
HBaseAdmin
(
conf
)
;
}
catch
(
Exception
e
)
{
logger
.
error
(
e
.
getMessage
(
)
,
e
)
;
throw
new
IllegalStateException
(
e
)
;
}
}
@
Override
public
HBaseAdmin
getHBaseAdmin
(
)
{
return
checkNotNull
(
hBaseAdmin
)
;
}
@
Override
public
HTableInterface
getHTable
(
String
tableName
)
{
checkArgument
(
isNotBlank
(
tableName
)
)
;
return
checkNotNull
(
hTablePool
.
getTable
(
tableName
)
)
;
}
@
Override
public
void
closeHTable
(
HTableInterface
hTableInterface
)
{
Closeables
.
closeQuietly
(
hTableInterface
)
;
}
@
Override
public
boolean
deleteTable
(
String
tableName
)
{
checkArgument
(
isNotBlank
(
tableName
)
)
;
try
{
hBaseAdmin
.
disableTable
(
tableName
)
;
hBaseAdmin
.
deleteTable
(
tableName
)
;
}
catch
(
IOException
e
)
{
logger
.
error
(
e
.
getMessage
(
)
,
e
)
;
return
false
;
}
return
true
;
}
@
Override
public
HTableDescriptor
createTable
(
String
tableName
,
int
maxVersion
)
{
return
createTable
(
tableName
,
"cf"
,
0
,
maxVersion
,
null
,
null
,
null
,
0
)
;
}
protected
HTableDescriptor
createTable
(
String
tableName
,
String
columnFamily
,
int
lifetime
,
int
maxVersion
,
StoreFile
.
BloomType
bloomType
,
String
startKey
,
String
endKey
,
int
numRegions
)
{
try
{
checkArgument
(
!
checkNotNull
(
hBaseAdmin
)
.
tableExists
(
tableName
)
,
"the table [%s] should not exist."
,
tableName
)
;
}
catch
(
IOException
e
)
{
logger
.
error
(
e
.
getMessage
(
)
,
e
)
;
throw
new
IllegalStateException
(
e
)
;
}
HColumnDescriptor
cf
=
getCF
(
columnFamily
,
lifetime
,
maxVersion
,
bloomType
)
;
HTableDescriptor
table
=
new
HTableDescriptor
(
tableName
)
;
table
.
addFamily
(
cf
)
;
try
{
if
(
StringUtils
.
isNotBlank
(
startKey
)
&&
StringUtils
.
isNotBlank
(
endKey
)
&&
numRegions
>
0
)
hBaseAdmin
.
createTable
(
table
,
Bytes
.
toBytes
(
startKey
)
,
Bytes
.
toBytes
(
endKey
)
,
numRegions
)
;
else
hBaseAdmin
.
createTable
(
table
)
;
}
catch
(
IOException
e
)
{
logger
.
error
(
e
.
getMessage
(
)
,
e
)
;
throw
new
IllegalStateException
(
e
)
;
}
return
describeTable
(
tableName
)
;
}
private
HColumnDescriptor
getCF
(
String
columnFamily
,
int
lifetime
,
int
maxVersion
,
StoreFile
.
BloomType
bloomType
)
{
HColumnDescriptor
cf
=
new
HColumnDescriptor
(
columnFamily
)
;
cf
.
setCompactionCompressionType
(
Compression
.
Algorithm
.
LZO
)
;
cf
.
setCompressionType
(
Compression
.
Algorithm
.
LZO
)
;
if
(
maxVersion
>
0
)
cf
.
setMaxVersions
(
maxVersion
>
1000000
?
1000000
:
maxVersion
)
;
if
(
lifetime
>
0
)
cf
.
setTimeToLive
(
lifetime
)
;
if
(
null
!=
bloomType
)
cf
.
setBloomFilterType
(
bloomType
)
;
else
cf
.
setBloomFilterType
(
StoreFile
.
BloomType
.
ROW
)
;
return
cf
;
}
public
HTableDescriptor
describeTable
(
String
tableName
)
{
try
{
return
checkNotNull
(
hBaseAdmin
)
.
getTableDescriptor
(
Bytes
.
toBytes
(
tableName
)
)
;
}
catch
(
Exception
e
)
{
logger
.
error
(
e
.
getMessage
(
)
,
e
)
;
throw
new
IllegalStateException
(
e
)
;
}
}
@
PreDestroy
public
void
destroy
(
)
throws
Exception
{
Closeables
.
closeQuietly
(
hTablePool
)
;
Closeables
.
closeQuietly
(
hBaseAdmin
)
;
}
}
|
1
2
3
4
5
6
7
|
HTableInterface
hTableInterface
=
null
;
try
{
hTableInterface
=
hBaseFactory
.
getHTable
(
"YOUR_TABLE_NAME"
)
;
// code here …
}
finally
{
hBaseFactory
.
closeHTable
(
hTableInterface
)
;
}
|
其次要注意filter不要太多,最好不要超过2个
具体的做法是:
开发过程中,难免需要加字段的,那这个时候,就需要代码、数据能够向下兼容。
比方说我们现在需要新增一个column,因为是新加的一列,原来的数据这列就是null,那么这时候从HBase里面读到的值就是null,所以写HBase代码一定要注意:
1
2
|
// firstNonNull 是 google guava Objects#firstNonNull 的方法,如果方法第一个参数是null,就返回第二个参数
Integer
.
parseInt
(
new
String
(
firstNonNull
(
result
.
getValue
(
DEFAULT_COLUMN_FAMILIES
,
COLUMN
)
,
new
byte
[
]
{
'0'
}
)
)
)
;
|
一般的做法是:
假设我们需要存储一个用户的所有微博(暂时不需要考虑时间倒排),这时候的RowKey的设计是UserId_WeiboId
,但是这样设计的话,UserId
的分布就很可能不均匀,因为RowKey是字符串排序的。
有两种办法来解决这个问题
UserId
字符串反转后存储UserId
MD5 后作取前6位为前缀加入到 UserId 前面UserId_WeiboId
建立一张索引表,并且这个表的Rowkey要和时间相关
当前时间 - 微博发表时间
的 long 值作为 RowKey 的前缀 那么RowKey会是 DayOfMonth_(当前时间 - 微博发表时间)
不过这样在代码实现上面的时候会有一些麻烦。