基本操作
HTable
提供了操作接口。建议应用程序初始化的2时候创建多个HTable
,每个线程要有一个,或者使用HTablePool
连接池。所有的修改只保证行级别的原子性。
以下是Java中操作HBase的例子,书中提供的代码已经deprecated,因此笔者用了最新的连接方式。这段代码运行一次要1分钟左右,主要耗时在连接部分,有配置可以跳过某些步骤没必要的步骤加快连接速度。
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.Table;
import org.apache.hadoop.hbase.util.Bytes;
import java.util.Map;
public class TestHBase {
public static void main(String[] argv) throws Exception {
// 建立连接
System.out.println("connecting...");
Configuration config = HBaseConfiguration.create();
Connection connection = ConnectionFactory.createConnection(config);
Table table = connection.getTable(TableName.valueOf("test"));
System.out.println("connect ok");
// 添加数据
Put one = new Put(Bytes.toBytes("Hello"));
one.addColumn(Bytes.toBytes("test_family"), Bytes.toBytes("name"), Bytes.toBytes("Alice"));
one.addColumn(Bytes.toBytes("test_family"), Bytes.toBytes("age"), Bytes.toBytes("20"));
table.put(one);
System.out.println("put ok");
// 查询
Get get = new Get(Bytes.toBytes("Hello"));
Result result = table.get(get);
Map<byte[], byte[]> a = result.getFamilyMap(Bytes.toBytes("test_family"));
String name = Bytes.toString(a.get(Bytes.toBytes("name")));
System.out.println("Get ok: " + name);
// 删除
Delete delete = new Delete(Bytes.toBytes("Hello"));
table.delete(delete);
System.out.println("delete ok");
// 断开连接
table.close();
connection.close();
}
}
KeyValue
类
KeyValue
类,表示一项数据,包括列族名、列、行健、时间戳、数据内容。是HBase中最基本的类,从注释中可以看到这个类不建议用户使用,仅限HBase内部使用。但是用户可以通过这个类访问原始数据,避免额外的复制操作,并且支持基于字节比较,效率更高。
KeyValue
类中有一些常用的比较器用于排序:
KeyComparator
,依据getKey
得到的结果进行比较。KVComparator
,与KeyComparator
相同。RowComparator
,依据getRow
得到的结果进行比较。MetaKeyComparator
,依据.META.
条目的行健进行比较MetaComparator
RootKeyComparator
,依据-ROOT-
条目的行健RootComparator
注意到这里有getKey
和getRow
,这两个方法的返回值都是byte[]
,它们有什么区别呢?getRow
得到的结果是行健,而getKey
得到的结果是原始行健,在getRow
之前还加上了点额外的数据。
KeyValue
中的getTypeByte
可获取操作类型。可能的类型有:
Put
Delete
DeleteColumn
DeleteFamily
所以从这里看看出,删除操作对于HBase而言也是往库里增加一条信息。
可以通过KeyValue.toString
查看类型,它的输出格式为:<row-key>/<family>:<qualifier>/<version>/<type>/<value-length>
。
客户端写缓冲
如果写数据一条一条写,网络开销会比较大,因此HBase客户端提供了多种批量写的方式。
第一种是开启写缓冲,开启方法为table.setAutoFlush(false)
。然后调用一批put
操作,最后flushCommits
批量提交请求。这样可以实现批量化。这种方式需要确保程序退出时必须调用flushCommits
,否则数据可能丢失。
第二种是通过Put
列表,一次性提交一批请求。
客户端在批量写的时候首先会根据row key进行排序,然后分割,发送到相应的region server上。
批量写还有一个问题,可能一批里面一部分数据写入失败了。这种情况会通过抛异常的方式将出错的信息包含在异常信息中。后面会讲到如何通过batch
方法更加灵活的进行错误处理。
客户端原子操作
即CAS。在写数据的时候带上前一个数据的值,确保原来的值没被修改过再写进去。这样可以让分布式系统支持并发的对同一条数据进行读写。如果想要写入的数据确保库中不存在,那么将前一个值设置为null即可。
get
操作
get
操作可以获取一条数据或者一批数据。先说获取一条数据。
get
方法的原型为Result get(Get get) throws IOException
。其中Get
类的构造方法原型有:
Get(byte[] row)
Get(byte[] row, RowLock rowLock)
可以看到row-key是必填项,行锁是可选项。Get
可以设置的查询条件有:
addFamily(byte[] family)
,添加需要获取的列族。addColumn(byte[] family, byte[] qualifier)
,添加需要获取的列。setTimeRange(long minStamp, long maxStamp) throws IOException
。设置时间戳范围。setTimeStamp(long time)
。获取特定的时间,本质上调用setTimeRange
。setMaxVersions(int maxVersions) throws IOException
,获取最大的版本数量,默认是1。setMaxVersions()
,获取所有版本。setFilter
,设置过滤条件。setCacheBlocks
,设置是否将块缓存到内存,已方便加载相邻的信息。顺序读取建议加上,随机读取不建议加。
Result
类。包含了get
操作的到的结果集。它有以下几个重要的成员方法:
byte[] getValue(byte[] family, byte[] qualifier)
:取得指定单元格的内容,只能取得最新版本。byte[] value()
:返回第一列、第一个版本的值。byte[] getRow()
:获取行健。int size()
:查询的到的结果数量。boolean isEmpty()
:查询结果是否为空。KeyValue[] raw()
:拿到底层原始KeyValue
结构List<KeyValue> list()
:将raw
得到的数据用List
进行封装。
Result
类中还有面向列的存取方式,可以获取一个列中包含的多个版本。
List<KeyValue> getColumn(byte[] family, byte[] qualifier)
:获取列对应的多个版本数据。KeyValue getColumnLatest(byte[] family, byte[] qualifier)
:获取列中最新版本的数据。boolean containsColumn(byte[] family, byte[] qualifier)
:是否版本指定的列。
Result
类中还有更方便的获取全部数据的方法:
NavigableMap<byte[], NaviableMap<byte[], NavigableMap<Long, byte[]>>> getMap()
:获取所有的列族、列、时间戳、数据。NavigableMap<byte[], NaviableMap<byte[], byte[]>> getNoversionMap()
:只获取最新版本的数据。
上面的get
方法只能获取一个key的数据,而get
列表方法可以批量取值。其原型为Result[] get(List<Get> gets) throws IOException
。这个方法一次可以获取一批数据。这一批数据中有任何一条数据取出时发生错误,则会抛异常。如果需要得到获取成功的那部分数据,则需要通过batch
方法,后面会讲。
获取数据相关的方法还有:
exists(Get get) throws IOException
。这个方法只判断数据是否存在,而不会把行键对应的具体内容返回给客户端。这样避免了一部分网络开销,但是服务端读取文件块这些操作是无法避免的。Result getRowOrBefore(byte[] row, byte[] family) throws IOException
。这个方法获取行键对应的数据或者这个行键之前的数据。因为HBase中所有的行键都是排序好的,因此获取行键之前的意思就是获取行键字母顺序更小的数据。
删除
删除方法的原型为:void delete(Delete delete) throws IOException
。这个方法最多只能删除一行数据。如果要删除多行就要调用多次。Delete
类的构造方法原型有:
Delete(byte[] row)
Delete(byte[] row, long timestamp, RowLock rowLock)
可以看到row key是必填的,时间戳和行锁是选填的。其中timestamp意思是删除此时间戳以及之前的数据,保留此时间戳之后的数据。
默认删除一整行,包括所有的列族、列、版本。可以通过以下方法缩小删除的范围:
Delete addFamily(byte[] family)
:只删除指定列族Delete addColumns(byte[] family, byte[] qualifier)
:只删除指定列的数据Delete addColumn(byte[] family, byte[] qualifier)
:只删除指定列中的最新版本void setTimestamp(long timestamp)
:设定时间戳,限定删除的范围是此时间戳以及之前。所以对于addColumns
和addFamily
而言,删除此时间戳以及之前的数据,对于addColumn
而言则是删除此时间戳或者之前数据中时间戳最晚的一条数据,如果没有符合条件的数据,则不删除。
删除操作也支持批量操作,其方法原型为:void delete(List<Delete> deletes) throws IOException
。
delete
操作也支持CAS特性,方法名字叫做compareAndDelete
。
批量操作
前面讲到的put
、delete
都支持批量操作。本质上它们都会调用底层的batch
方法实现。其方法原型为
void batch(List<Row> actions, Object[] results) throws IOException, InterruptedException
Object[] batch(List<row> actions) throws IOException, InterruptedException
这里面出现了Row
类(实际上它是接口),这是因为Put
类、Delete
类、Get
类都实现了Row
接口。以上两种方法第二种方法已被标记为deprecated。它们的区别在于当客户端抛出异常时,比如当请求执行了一半时,当前线程发生了中断,那么第一种方法可以拿到执行了前半部分的指定结果,而第二种方法的执行结果没办法取到。
需要注意的是不要把Put
请求和Delete
请求放在同一批请求中,在某些情况下会产生随机效果。
batch
方法的返回结果是一个Object[]
,它里面的数据类型有以下几种:
null
:通信失败EmptyResult
:Put
和Delete
操作成功返回的结果Result
:Get
操作成功返回的结果。如果没有匹配的行,则会返回空的Result
Throwable
:在服务端执行时出现了错误,可能是参数有问题,也可能是服务端出现故障
batch
方法直接将请求发送给服务端,不受写缓冲的控制。
batch
方法会对暂时性的错误进行重试,默认重试10次。可在配置项hbase.client.retries.number
中调整。
行锁
前面讲到的CAS特性是一种乐观锁,当并发冲突较少时可采用CAS方案。当并发冲突经常发生时,可以采用悲观锁的方案。其方法为:
RowLock lockRow(byte[] row)
:锁定一行void unlockRow(RowLock rowLock)
:解锁一行
当一行锁定时,别的客户端不能写入也不能读取,只能等到锁释放之后才能继续。锁有租期限制,默认120秒,可通过配置hbase.regionserver.lease.period
调整。
通常行锁不建议使用,容易导致死锁等问题。
get
操作不会拿到写了一半的数据,所有行级别的操作都是原子性的。实现方式是通过MCC实现的(维基词条: https://en.wikipedia.org/wiki/Multiversion_concurrency_control ),就是说修改一行时,不会覆盖原来的数据,而是新开一个版本写入,写完之后才给别的客户端可见。
扫描
通过scan
操作可以获取连续一批行健。其方法原型为ResultScanner getScanner(Scan scan) throws IOException
。其中Scan
类的构造函数原型为:
Scan()
Scan(byte[] startRow, Filter filter)
Scan(byte[] startRow)
Scan(byte[] startRow, byte[] stopRow)
可以看到扫描操作可以设置起止行健、过滤器。返回的结果集中包含行健为startRow
的数据,不包含stopRow
的数据。也就是左闭右开区间。
Scan
类中还可以增加限定条件,方法如下。由于HBase是列式存储,如果限定了获取的列,那么开销就会相应减少。
Scan addFamily(byte[] family)
:只获取指定列族的数据Scan addColumn(byte[] family, byte[] qualifier)
:只获取指定列的数据
ResultScanner
负责批量从服务器中取数据,然后通过如下的接口让用户获取:
Result next() throws IOException
:获取下一条数据,如果没有了就返回null
。每次调用都会发生一个RPC请求。Result[] next(int nbRows) throws IOException
:获取后面nbRows
条数据。如果到底了,返回的数组长度可能比nbRows
小。每次调用都会发生RPC请求。void close()
:扫描结束后要关掉。必须要关掉,否则时间长了占用资源。扫描器也有租约限制,默认是120秒,可通过hbase.regionserver.lease.period
调整。
可以看到hbase.regionserver.lease.period
同时控制扫描器和行锁的租期限制。
前面提到了next
方法每次都会发出RPC请求。这个行为可以通过配置做优化,有一下两种方法。
Table.setScannerCaching(int scannerCaching)
:表级别设置缓冲数量,对这张表所有的扫描都生效。Scan.setCache(int caching)
:扫描器级别设置缓冲数量,仅对本次扫描生效。
设置缓冲之后,第一次调用next
会发起一次RPC,然后后续几次next
直接在客户端内部完成操作,直到缓冲区中的数据读完完毕,又会发起一个RPC请求。因此缓冲数量过大则会导致next
方法性能不稳定,有时快有时慢。
还有一种情况,列非常多,内存放不下怎么办?可以通过Scan.setBatch(int batchSize)
参数控制每次返回的列数。比如一行数据有13列,如果batch
设置为5,那么这一条数据会拆成3个Result
返回,分别包含5、5、3列数据。
获取Region的物理分布
通过Connection.getRegionLocator
得到RegionLocator
,然后通过RegionLocator.getAllRegionLocations
得到所有的分区物理分布。