使用 Java 获取 HBase 中多版本数据的方法

最近工作比较繁忙,在处理需求、写代码的过程中踩到了一些坑,不过问题都被我一个一个解决了,所以最近三周都没有更新博客内容。不过,我是整理了提纲、打了草稿,近期会陆续整理出来。今天就先整理出来一个简单的知识点:使用 Java API 从 HBase 中获取多版本【Version 的概念】数据的方法,开发环境基于 JDK v1.8、HBase v1.1.2、Zookeeper v3.4.6,在演示过程中还会使用原生的 HBase Shell 进行配合,加深理解。

入门概念

先列举一些关于 HBase 的基础概念,有助于继续阅读下文,如果不太了解需要先回顾一下:

  • 列式分布式数据库,基于 Google BigTable 论文开发,适合海量的数据存储
  • Rowkey、Column Family、Qualifier、Timestamp、Cell、Version 的概念
  • HBase Shell、Java API、Phoenix

示例代码

下面的演示会以 HBase Shell、Java API 这两种方式分别进行,便于读者理解。

建表造数据

为了使用 Java API 获取多版本数据,我要先做一些基础工作:创建表、造数据、造多版本数据。为了尽量简化数据的复杂度,以及能让读者理解,我准备了 2 条数据,下面使用一个表格来整理这 2 条数据,读者可以看得更清晰:

RowkeyColumn FamilyQualifierVersionValue
row01cfname1JIM
row01cfname2Jack
row02cfname1Lucy
row02cfage120

从上表可以看出,一共 2 条数据,row01 有 1 列,2 个版本,row02 有 2 列,1 个版本。下面使用原生的 HBase Shell 开始逐步建表、造数据。

1、进入交互式客户端

使用 hbase shell 进入交互式客户端,在输出的日志中可以看到当前环境 HBase 的版本号。

登录成功后终端显示:

登录成功

2、创建表:学生表

使用 create 'TB_HBASE_STUDENT','cf' 创建一张表,为了便于后面的操作,表名最好使用大写形式,否则涉及到表名的操作需要加单引号。由于 HBase 是列式存储结构,所以创建表时不需要指定具体的列名称,只要指定 Column Family 名称即可。

执行后终端显示:

1
2
0 row (s) in 2.5260 seconds
=> Hbase::Table - TB_HBASE_STUDENT

创建表

3、查看表结构

使用 describe 'TB_HBASE_STUDENT' 查看表结构,执行后终端显示:

1
2
3
4
5
Table TB_HBASE_STUDENT is ENABLED
TB_HBASE_STUDENT
COLUMN FAMILIES DESCRIPTION
{NAME => 'cf', BLOOMFILTER => 'ROW', VERSIONS => '1', IN_MEMORY => 'false', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', TTL => 'FOREVER', COMPRESSION => 'NONE', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'}
1 row (s) in 0.0390 seconds

查看表结构

可以看到表的基本信息,其中 Column Family 名称为 cf,最大版本 VERSIONS 为 1,这会导致只会存储一个版本的列数据,当再次插入数据的时候,后面的值会覆盖掉前面的值。

4、修改最大版本

为了满足我的需求,需要更改表,把 cf 的最大版本数 VERSIONS 增加,设置为 3 。使用 alter 'TB_HBASE_STUDENT',{NAME=>'cf',VERSIONS=>3} 命令即可。执行后终端显示:

1
2
3
4
5
Updating all regions with the new schema...
0/1 regions updated.
1/1 regions updated.
Done.
0 row (s) in 3.7710 seconds

修改最大版本数

修改成功后,我使用 describe 'TB_HBASE_STUDENT' 再次查看表结构,终端显示:

1
2
3
4
5
Table TB_HBASE_STUDENT is ENABLED
TB_HBASE_STUDENT
COLUMN FAMILIES DESCRIPTION
{NAME => 'cf', BLOOMFILTER => 'ROW', VERSIONS => '3', IN_MEMORY => 'false', KEEP_DELETED_CELLS => 'FALSE', DATA_BLOCK_ENCODING => 'NONE', TTL => 'FOREVER', COMPRESSION => 'NONE', MIN_VERSIONS => '0', BLOCKCACHE => 'true', BLOCKSIZE => '65536', REPLICATION_SCOPE => '0'}
1 row (s) in 0.0380 seconds

再次查看表结构

这次可以看到,VERSIONS => '3' 表示 cf 已经支持存储 3 个版本的数据了。

5、插入 2 条数据

HBase 的插入数据功能是使用 put 命令,每次插入 1 列,根据上述表格数据格式,需要执行 4 次 put 操作。

1
2
3
4
put 'TB_HBASE_STUDENT','row01','cf:name','JIM'
put 'TB_HBASE_STUDENT','row01','cf:name','Jack'
put 'TB_HBASE_STUDENT','row02','cf:name','Lucy'
put 'TB_HBASE_STUDENT','row02','cf:age','20'

执行后终端显示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
1.8.7-p357 :012 >   put 'TB_HBASE_STUDENT','row01','cf:name','JIM'
0 row (s) in 0.1600 seconds

1.8.7-p357 :013 > put 'TB_HBASE_STUDENT','row01','cf:name','Jack'
0 row (s) in 0.0180 seconds

1.8.7-p357 :014 > put 'TB_HBASE_STUDENT','row02','cf:name','Lucy'
0 row (s) in 0.0160 seconds

1.8.7-p357 :015 > put 'TB_HBASE_STUDENT','row02','cf:age','20'
0 row (s) in 0.0180 seconds

1.8.7-p357 :016 >

插入 2 条数据

命令行查看

1、先尝试使用 get 命令来获取这 2 条数据,分别执行 3 次 get 操作。

1
2
3
get 'TB_HBASE_STUDENT','row02','cf:name'
get 'TB_HBASE_STUDENT','row02','cf:age'
get 'TB_HBASE_STUDENT','row01','cf:name'

执行后终端显示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.8.7-p357 :026 >   get 'TB_HBASE_STUDENT','row02','cf:name'
COLUMN CELL
cf:name timestamp=1566118670447, value=Lucy

1 row (s) in 0.0160 seconds

1.8.7-p357 :027 > get 'TB_HBASE_STUDENT','row02','cf:age'
COLUMN CELL
cf:age timestamp=1566118677185, value=20

1 row (s) in 0.0060 seconds

1.8.7-p357 :028 > get 'TB_HBASE_STUDENT','row01','cf:name'
COLUMN CELL
cf:name timestamp=1566118661397, value=Jack

1 row (s) in 0.0080 seconds

1.8.7-p357 :029 >

读取数据

可以看到,此时并没有获取到 row01 的 2 个版本的数据,只获取了最新版本的结果。

2、使用 get 获取多版本数据,执行 get 时需要加上 VERSIONS 相关的参数。

1
get 'TB_HBASE_STUDENT','row01',{COLUMN=>'cf:name',VERSIONS=>3}

执行后终端显示如下:

1
2
3
4
5
6
7
8
1.8.7-p357 :029 > get 'TB_HBASE_STUDENT','row01',{COLUMN=>'cf:name',VERSIONS=>3}
COLUMN CELL
cf:name timestamp=1566118661397, value=Jack
cf:name timestamp=1566118652009, value=JIM

2 row (s) in 0.0140 seconds

1.8.7-p357 :030 >

读取多版本数据

可以看到,2 个版本的数据都读取出来了。

3、使用 scan 扫描数据

此外还有一个 scan 命令可以扫描表中的数据,使用 scan 'TB_HBASE_STUDENT',{LIMIT=>5} 尝试扫描 5 条数据出来。

执行后终端显示如下:

1
2
3
4
5
6
7
8
9
1.8.7-p357 :031 >   scan 'TB_HBASE_STUDENT',{LIMIT=>5}
ROW COLUMN+CELL
row01 column=cf:name, timestamp=1566118661397, value=Jack
row02 column=cf:age, timestamp=1566118677185, value=20
row02 column=cf:name, timestamp=1566118670447, value=Lucy

2 row (s) in 0.0420 seconds

1.8.7-p357 :032 >

扫描数据

由于表中只有 2 条数据,所以只显示出 2 条,而且 scan 默认也是获取最新版本的数据结果。

4、如果想退出 HBase Shell 交互式客户端,使用 !quit 命令即可。

退出客户端

代码示例

上面使用原生的 HBase Shell 操作演示了创建表、插入数据、读取数据的过程,下面将使用 Java API 演示读取数据的过程,而创建表、插入数据的过程就不再演示。

这里需要特别注意,为了正常使用 Java API 的相关接口,Java 项目需要依赖 hbase-client、commons-configuration、hadoop-auth、hadoop-hdfs 等组件。我的代码已经上传至 GitHub,详见:TestHBase.java ,搜索类名 TestHBase 即可。

1、代码示例

代码结构比较简单,分为:构造查询请求、发送请求、解析结果输出几部分,注释中也注明了各个部分的作用,总计也就 50 行代码左右。

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
/**
* HBase Java API Get 测试
*/
public void testGet () {
String hTableName = "TB_HBASE_STUDENT";
IplaypiStudyConfig configuration = IplaypiStudyConfig.getInstance ();
byte [] cfbyte = "cf".getBytes ();
byte [] col01byte = "name".getBytes ();
byte [] col02byte = "age".getBytes ();
try {
// 构造查询请求,2 条数据,多个版本
List<Get> getList = Lists.newArrayList ();
Get get = new Get (Bytes.toBytes ("row01"));
get.addColumn (cfbyte, col01byte);
// 设置最大版本数,默认为 1
get.setMaxVersions (3);
getList.add (get);
Get get2 = new Get (Bytes.toBytes ("row02"));
get2.addColumn (cfbyte, col01byte);
get2.addColumn (cfbyte, col02byte);
getList.add (get2);
// 发送请求,获取结果
HTable hTable = new HTable (configuration, hTableName);
Result [] resultArr = hTable.get (getList);
/**
* 以下有两种解析结果的方法
* 1 - 通过 Result 类的 getRow () 和 getValue () 两个方法,只能获取最新版本
* 2 - 通过 Result 类的 rawCells () 方法返回一个 Cell 数组,可以获取多个版本,如果使用 getColumnCells 可以指定列
* 注意,高版本不再建议使用 KeyValue 的方式,注释中有说明
*/
// 1-
log.info ("====get result by first method");
for (Result result : resultArr) {
log.info ("");
log.info ("--------");
String rowStr = Bytes.toString (result.getRow ());
log.info ("====row:[{}]", rowStr);
// 如果包含 name 列,则获取输出
if (result.containsColumn (cfbyte, col01byte)) {
String valStr = Bytes.toString (result.getValue (cfbyte, col01byte));
log.info ("====name:[{}],getValue", valStr);
// 以下方式不建议使用,但是可以获取多版本
List<KeyValue> keyValueList = result.getColumn (cfbyte, col01byte);
for (KeyValue keyValue : keyValueList) {
log.info ("====name:[{}],getColumn -> getValue", Bytes.toString (keyValue.getValue ()));
}
}
// 如果包含 age 列,则获取输出
if (result.containsColumn (cfbyte, col02byte)) {
String valStr = Bytes.toString (result.getValue (cfbyte, col02byte));
log.info ("====age:[{}],getValue", valStr);
// 以下方式不建议使用,但是可以获取多版本
List<KeyValue> keyValueList = result.getColumn (cfbyte, col02byte);
for (KeyValue keyValue : keyValueList) {
log.info ("====age:[{}],getColumn -> getValue", Bytes.toString (keyValue.getValue ()));
}
}
}
// 2-
log.info ("");
log.info ("====get result by second method");
for (Result result : resultArr) {
log.info ("");
log.info ("--------");
String rowStr = Bytes.toString (result.getRow ());
log.info ("====row:[{}]", rowStr);
//name 列
List<Cell> cellList = result.getColumnCells (cfbyte, col01byte);
// 1 个 cell 就是 1 个版本
for (Cell cell : cellList) {
// 高版本不建议使用
log.info ("====name:[{}],getValue", Bytes.toString (cell.getValue ()));
//getValueArray: 数据的 byte 数组
//getValueOffset:rowkey 在数组中的索引下标
//getValueLength:rowkey 的长度
String valStr = Bytes.toString (cell.getValueArray (), cell.getValueOffset (), cell.getValueLength ());
log.info ("====name:[{}],[getValueArray,getValueOffset,getValueLength]", valStr);
log.info ("====timestamp:[{}],cell", cell.getTimestamp ());
}
//age 列不演示了,省略...
}
}
catch (IOException e) {
log.error ("!!!!error: " + e.getMessage (), e);
}
}

2、运行结果

执行运行,可以看到结果输出,与数据表中一致,多版本数据结果也可以全部获取:

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
2019-08-18_17:54:18 [main] INFO test.TestHBase:58: ====get result by first method
2019-08-18_17:54:18 [main] INFO test.TestHBase:60:
2019-08-18_17:54:18 [main] INFO test.TestHBase:61: --------
2019-08-18_17:54:18 [main] INFO test.TestHBase:63: ====row:[row01]
2019-08-18_17:54:18 [main] INFO test.TestHBase:67: ====name:[Jack],getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:71: ====name:[Jack],getColumn -> getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:71: ====name:[JIM],getColumn -> getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:60:
2019-08-18_17:54:18 [main] INFO test.TestHBase:61: --------
2019-08-18_17:54:18 [main] INFO test.TestHBase:63: ====row:[row02]
2019-08-18_17:54:18 [main] INFO test.TestHBase:67: ====name:[Lucy],getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:71: ====name:[Lucy],getColumn -> getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:77: ====age:[20],getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:81: ====age:[20],getColumn -> getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:86:
2019-08-18_17:54:18 [main] INFO test.TestHBase:87: ====get result by second method
2019-08-18_17:54:18 [main] INFO test.TestHBase:89:
2019-08-18_17:54:18 [main] INFO test.TestHBase:90: --------
2019-08-18_17:54:18 [main] INFO test.TestHBase:92: ====row:[row01]
2019-08-18_17:54:18 [main] INFO test.TestHBase:98: ====name:[Jack],getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:103: ====name:[Jack],[getValueArray,getValueOffset,getValueLength]
2019-08-18_17:54:18 [main] INFO test.TestHBase:104: ====timestamp:[1566118661397],cell
2019-08-18_17:54:18 [main] INFO test.TestHBase:98: ====name:[JIM],getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:103: ====name:[JIM],[getValueArray,getValueOffset,getValueLength]
2019-08-18_17:54:18 [main] INFO test.TestHBase:104: ====timestamp:[1566118652009],cell
2019-08-18_17:54:18 [main] INFO test.TestHBase:89:
2019-08-18_17:54:18 [main] INFO test.TestHBase:90: --------
2019-08-18_17:54:18 [main] INFO test.TestHBase:92: ====row:[row02]
2019-08-18_17:54:18 [main] INFO test.TestHBase:98: ====name:[Lucy],getValue
2019-08-18_17:54:18 [main] INFO test.TestHBase:103: ====name:[Lucy],[getValueArray,getValueOffset,getValueLength]
2019-08-18_17:54:18 [main] INFO test.TestHBase:104: ====timestamp:[1566118670447],cell

Java 程序运行结果

备注

1、在使用 Java API 时注意低版本、高版本之间的差异,必要时及时升级,就像上文代码中的 Result.getColumn、KeyValue.getValue ()、Cell.getValue () 这几个方法。

2、Phoenix 是一款基于 HBase 的工具,在 HBase 之上提供了 OLTP 相关的功能,例如完全的 ACID 支持、SQL、二级索引等,此外 Phoenix 还提供了标准的 JDBC 的 API。在使用 Phoenix 时,可以很方便地像操作 SQL 那样操作 HBase。

使用 Phoenix 创建表、查询数据示例如图。

创建表,使用:
CREATE TABLE IF NOT EXISTS TB_HBASE_STUDENT ("pk"varchar primary key,"cf"."name"varchar,"cf"."age"varchar);

使用 Phoenix 创建表

查询示例,使用:
select * from"TB_HBASE_STUDENT"limit 5;

使用 Phoenix 查询

3、本示例的代码放在 GirHub,详见:TestHBase.java ,搜索类名 TestHBase 即可。参考 GitHub 的代码时,注意在 iplaypistudy-common-config 模块中增加自己的配置文件,如果开发环境的版本不匹配,也要升级版本,在 pom.xml 更改即可。

4、想要使用 HBase Shell 删除表时,必须先使用 disable YOUR_TABLE_NAME 来禁用表,然后再使用 drop YOUR_TABLE_NAME 删除表,直接删除表是不被允许的。

虾丸派 wechat
扫一扫添加博主,进技术交流群,共同学习进步
永不止步
0%