之前读《HBase权威指南》在实践时,发现API已经发生了一些变化,查阅官方文档,确认HBase的API在1.0版本后已经做了修改。本文介绍在新API下,使用Java访问HBase的方法。
HBase Client通过查询hbase:meta表来确定你所感兴趣的数据行所在的RegionServers。在定位到这些数据所在的region后,client会直接和这些region所在的RegionServer联系进行数据的读写,而不会经由master进行数据的读写。Client还会缓存这些信息以使随后的其他请求不需要再次经过这样的查询过程。Client只有在感知到因为master触发了负载均衡导致region重新分配,或是RegionServer挂掉后,才会重新查询定位请求读写的region。
1. 配置Configuration,获取Connection实例
通过Client操作HBase,需要配置链接的参数,获取Connection实例,然后从该实例中获取用户操作HBase的Table,Admin等对象。Connection实例是重量级的对象,但是它是线程安全的,因此client端可以只创建并维护好一个该实例。当所有的操作完成后,client应该负责关闭该实例。Table,Admin这些对象是轻量级的,当需要时就创建它,并在用完后释放。
Configuration config = HBaseConfiguration.create();
config.set("hbase.zookeeper.quorum", "aa.bb.cc.dd");
config.set("hbase.zookeeper.property.clientPort", "2181");
config.set("hbase.rootdir","hdfs://aa.bb.cc.dd:9000/hbase");
config.set("hbase.table.sanity.checks", "false");
Connection conn = ConnectionFactory.createConnection(config);
如上,创建并配置Configuration对象,并将其传递给ConnectionFactory,从中获取Connection实例。Configuration对象参数的配置可以参考hbase-site.xml以及HDFS相关配置。ConnectionFactory是一个”non-instantiable class”,用于管理Connection的创建。
2. namespace
可以通过namespace指定表属于的命名空间,建表时如若不指定,则默认放在default。namespace指的是一个表的逻辑分组,可以更好的实现数据的隔离和同一类表的资源管理。
Connection conn = null;
Admin admin = null;
try {
conn = ConnectionFactory.createConnection(config);
admin = conn.getAdmin();
NamespaceDescriptor[] nsList = admin.listNamespaceDescriptors();
boolean found = false;
for(NamespaceDescriptor ns:nsList){
System.out.println("Namespace:" + ns.getName());
if(ns.getName() != null && ns.getName().equals(nsName)){
found = true;
break;
}
}
if(found){
System.out.println("namespace:" + nsName +" exist, do not need to create");
return;
}
admin.createNamespace(NamespaceDescriptor.create(nsName).build());
System.out.println("Finish to create namespace");
} catch (Exception e) {
} finally{
if(null != conn) {try{conn.close();}catch (Exception e) {e.printStackTrace();}}
if(null != admin) {try{admin.close();}catch (Exception e) {e.printStackTrace();}}
}
上述代码展示了,如何查找表空间,以及创建表空间。在新版本的API中Admin对象代替了原来的HBaseAdmin用于create/drop/list/enable/disable /modify表操作,当然也会执行其他管理性操作(compact/closeRegion等),在这里使用了admin的createNamespace方法来创建表空间。NamespaceDescriptor用于描述Namespace。
3. 创建/修改表
/*
*nsName:表空间名
*tableName:表名称
*/
admin = conn.getAdmin();
HTableDescriptor table = new HTableDescriptor(TableName.valueOf(nsName + ":" + tableName));
//如果表已经存在,先disable,再delete,然后重新创建
if(admin.tableExists(table.getTableName())){
admin.disableTable(table.getTableName());
admin.deleteTable(table.getTableName());
}
table.setDurability(Durability.SYNC_WAL);
//创建列簇
HColumnDescriptor hcd = new HColumnDescriptor("info-test");
hcd.setCompressTags(false);
hcd.setMaxVersions(3);
table.addFamily(hcd);
admin.createTable(table);
HTableDescriptor,HColumnDescriptor分别用户描述表和列簇。如需删除表,需要先将表disableTable而后才能执行deleteTable操作。HColumnDescriptor包含了列簇所含的最大版本个数,压缩设置等。
//remove column family
admin.disableTable(table.getTableName());
table.remove(Bytes.toBytes("info-test"));
//add
HColumnDescriptor hcd = new HColumnDescriptor("info");
hcd.setMaxVersions(3);
table.addFamily(hcd);
//modify&enable
admin.modifyTable(table.getTableName(), table);
admin.enableTable(table.getTableName());
上述代码描述了怎样删除原有列簇,新增新的列簇;对于表的修改要坚守先disableTable在修改,最后enableTable的步骤。
4. Put/Get向表中存取数据
Put:向表中存入数据
/*
*nsName:表空间名
*tableName:表名称
*/
Admin admin = null;
Table table = null;
try {
admin = conn.getAdmin();
TableName tName = TableName.valueOf(nsName + ":" + tableName);
if(!admin.tableExists(tName)){
return;
}
table = conn.getTable(tName);
ArrayList puts = new ArrayList<>();
Put put = new Put(Bytes.toBytes("row-1"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("LinTao"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("28"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("school"), Bytes.toBytes("NJUPT"));
puts.add(put);
put = new Put(Bytes.toBytes("row-2"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("LinLei"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"), Bytes.toBytes("29"));
put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("school"), Bytes.toBytes("NJU"));
puts.add(put);
table.put(puts);
} catch (Exception e) {
} finally{
if(null != conn) {try{conn.close();}catch (Exception e) {e.printStackTrace();}}
if(null != admin) {try{admin.close();}catch (Exception e) {e.printStackTrace();}}
if(null != table) {try{table.close();}catch (Exception e) {e.printStackTrace();}}
}
上面描述了怎样向指定表空间的表中插入数据。新的Table对象已经替代了原来的HTable对象,用于HBase表通信,执行get/put/delete/scan data from a table.Put用于创建一行数据,其构造函数的参数即是rowkey,良好的rowkey设计可以极大的提升HBase的存取性能。addColumn用于向指定列簇,限定符中添加列。设置或添加行的其他属性的方法还有:
add(Cell cell) //Add the specified KeyValue to this Put operation.
addColumn(byte[] family, byte[] qualifier, long ts, byte[] value) //Add the specified column and value, with the specified timestamp as its version to this Put operation.
setTimestamp(long timestamp) //Set the timestamp of the delete.
setTTL(long ttl) //Set the TTL desired for the result of the mutation, in milliseconds.
……
每一个put操作实际上都是一个RPC操作,它将客户端数据传送到服务器然后返回。如果有大量的这种put提交势必会影响性能。HBase的API为客户端配置了写缓冲区,缓冲区收集put操作然后调用RPC一次性将多个put送往服务器。可以设置缓冲区自动flush数据到服务端(table.setAutoFlush(true)),但这个通常不是最好的选择,默认情况下AutoFlush时关闭的。API会追踪统计当前缓冲区数据的大小,达到门限后会自动刷写数据。此外当我们调用table.close()时也会触发一次无条件的刷写。
Get:获取数据
table = conn.getTable(tName);
//get
Get get = new Get(Bytes.toBytes("row-1"));
Result result = table.get(get);
for(Cell cell:result.rawCells()){
System.out.println(
"RowKey :" + Bytes.toString(result.getRow()) +
"Familiy:Qualifir :" + Bytes.toString(CellUtil.cloneQualifier(cell))+
"Value:" + Bytes.toString(CellUtil.cloneValue(cell)));
}
获取数据可以使用Get,可以设置Get的参数用户获取指定行,也可以指定列簇名,指定qualifier,指定timestamp等
addColumn(byte[] family, byte[] qualifier)//Get the column from the specific family with the specified qualifier.
addFamily(byte[] family)//Get all columns from the specified family.
setCacheBlocks(boolean cacheBlocks)//Set whether blocks should be cached for this Get.
setTimeRange(long minStamp, long maxStamp)//Get versions of columns only within the specified timestamp range, [minStamp, maxStamp).
setTimestamp(long timestamp)//Get versions of columns with the specified timestamp.
通过指定列簇名和限定符直接获取结果值:
get.addColumn(Bytes.toBytes("row-1"), Bytes.toBytes("age"));
result = table.get(get);
result.getValue(Bytes.toBytes("info"), Bytes.toBytes("age"));
5. delete删除数据
常用构造函数
Delete(byte[] row) //指定要删除的行键
删除行键指定行的数据。如果没有进一步的设置,使用该构造函数将删除行键指定的行中 所有列族中所有列的所有版本 !
Delete(byte[] row, long timestamp) //删除行键和时间戳共同确定行的数据
常用方法:
deleteColumn(byte[] family, byte[] qualifier) //删除指定列的 最新版本 的数据。
deleteColumns(byte[] family, byte[] qualifier) //删除指定列的 所有版本的数据。
deleteFamily(byte[] family) //删除指定列族的所有列的 所有 版本数据。
//删除指定行,列簇中指定qualified的字段值
table = conn.getTable(tName);
Delete delete = new Delete(Bytes.toBytes("row-1"));
delete.addColumn(Bytes.toBytes("info"), Bytes.toBytes("age"));
table.delete(delete);
6. Scan:扫描
table = conn.getTable(tName);
//scanner
Scan scan = new Scan();
scan.addFamily(Bytes.toBytes("info"));
ResultScanner rs = table.getScanner(scan);
for(Result r = rs.next(); r != null; r =rs.next()){
for(Cell cell:r.rawCells()){
System.out.println( CellUtil.cloneRow(cell)
+ " => "+ Bytes.toString(CellUtil.cloneFamily(cell))
+ "," + cell.getTimestamp()
+ ", {" + Bytes.toString(CellUtil.cloneQualifier(cell))
+ ":" + Bytes.toString(CellUtil.cloneValue(cell))+ "}");
}
}
scan通常意味了消耗更多的性能,我们只展示Scan方法的使用,工程实践中,应该通过设置过滤器,限定时间范围,指定起始列,配置缓存等要优化。
对ResultScanner的每一次next()调用都会为每行数据生成一个单独的RPC请求,如果想让一次PRC调用获得多行数据,则需要开启扫描器缓存:
//Set the number of rows for caching that will be passed to scanners.
setCaching(int caching)
setCaching可以控制每次RPC取回的行数,但是如果设置的过大,导致返回给客户端的数据超出其堆的大小,程序就会抛出OOM
//Set the maximum number of cells to return for each call to next().
setBatch(int batch)
setBatch用于控制每次取回的列数,因为如果某行数据量非常大,返回的这些行有可能超过客户端进程的内存容量。
综上缓存是面向行一级操作的优化,批量则是面向列一级的操作。