一、环境
在Hadoop上快速搭建Hbase环境非常简单,下载软件包解压以后写好配置文件就可以直接运行了,不多作叙述。
二、基础客户端API
1、Bytes类
Hbase中所有的值都是以字节数组的形式存在的,所以在使用客户端API和Hbase交互的过程中难免会经常的要操作字节数组,这里Hbase提供了一个工具类,Bytes类。其中有很多的静态方法可以方便的将各种类型的值转换成字节数组,同时也可以将字节数组转换成对应的类型。
2、KeyValue类
KeyValue提供了一个Hbase中键值对的抽象,我们都知道Hbase是面向列的数据库,其中的数据是以键值对K-V的形式存在,KeyValue就是一个完全的抽象。KeyValue封装了一个单元数据的完整坐标(Key):主键(row key)、簇(Family)、列(Qualifier),版本(Timestamp)和值(Value)
3、Put方法
Hbase提供一个Put类,用于封装插入的逻辑。插入时必须制定主键(Row Key)的值,所以构造方法中必须提供主键值。
- Put(byte[] row)
- Put(byte[] row, RowLock)
- ...
有了Put对象以后,就可以在对象中添加插入的信息
- Put add(byte[] family, byte[] qualifier, byte[] value);
- Put add(byte[] family, byte[] qualifier, long ts, byte[] value); 为了提高执行效率,插入的操作可以设定一个缓冲区,缓存要插入的信息,超过阀值后才提交,减少和Server交互的次数。
缓冲区:缓冲区默认是关闭的,要调用table.setAutoFlush(true)显示的开启。并可以通过table.setWriteBufferSize(size)设定缓冲区的大小。除了这种方式以外,配置文件中提供静态的配置项可以配置缓冲区的大小。
开启缓冲区以后需要刷新才能让请求提交到Server。
显式的提交(Explicit Flush):调用table.flushCommits()方法可以显式的将缓冲区里的请求提交到Server
隐式的提交 (Implicit Flush) :每一次调用table.put()或者table.setWriteBufferSize()方法都会触发一个检查当前的缓冲区是否超过阀值,如果超过了就会执行一次隐式的提交。另外,调用table.close()方法会强制将缓冲区内的请求提交。
批量提交(List of Puts):Table提供批量提交的功能,可以一次提交多个插入请求
- void put(List<Put> puts)
值得注意的是,这里实际提交的顺序是一定的,但是在服务器被执行的顺序却是不可预知的,因为服务器会根据负载等情况对请求的执行作优化,所以如果提交的请求对顺序有其他的要求应该作更加精细的提交。
如果在批量提交的过程中有某些请求出现了错误(提交的数据不符合表定义、服务器故障),不会影响其他正常的请求,它们将被正常的提交并写入数据库。那些出现了错误的请求将导致客户端抛出异常,且没有成功执行的请求会被重新放进客户端的缓冲区(Write Buffer)里,等待客户端作进一步的处理。
CAS(Compare And Set)插入(这是一种无锁的安全性保证):
CAS插入也是被支持的,客户端可以在插入前作比较确认目标记录没有被其他的请求修改再执行插入。这样的执行方式可以保证当你作read-bussiness-write的时候不会因为read之后有其他并行线程修改数据而导致你的业务出现错误。这是一种强大的执行方式,在并行、分布式的业务中。
4、Get方法
Hbase提供一个get方法来获取到指定主键的值。
- Get(byte[] row)
- Get(byte[] row, RowLock rowLock)
由于方法功能定义的限制,所以构造方法里都要求指定一个主键。查找的过程中,还可以附加其他的一些条件如簇名(Family),列名,版本等。
- Result get(Get get)
HTable的get方法会返回一个Result类,封装了获取的值的全部信息。Result提供了三种方法来遍历数据
- byte[] getValue(byte[] family, byte[] qualifier)
- KeyValue[] raw()
- List<KeyValue> list()
即,可以通过指定簇名和列名来得到指定的值,或者把整个返回结果转换成为数组或List来遍历。不管是哪一种方法,都可以获取到全部的数据,可以根据需求灵活运用。
批量提交(List of Gets):同put一样,get也有批量提交的方法
- Result[] get<List<Get> gets>
不同于put的是,一旦集合中有一个无效的get请求将返回一个exception,而所有的get请求都将失效而不会返回任何数据。
Delete方法
Hbase提供delete方法来删除指定的数据
类似于get方法,delete方法需要主键,而其他的条件是任选的,但是不同的条件将带来不同的执行结果。
- Delete deleteFamily(byte[] family)
- Delete deleteColumns(byte[] family, byte[] qualifier)
- Delete deleteColumn(byte[] family, byte[] qualifier)
没有任意任选条件被调用时,将删除所有具有row key的数据
当指定的是单一的column时,对应column的最近一个版本的数据将被删除
当指定的是columns时,对应的column的所有值将被删除
当指定的是family时,对应的整个family的所有值将被删除
Delete也有类似的批量操作的方法,同样的,执行顺序是不可预知的。
Delete还有类似的CAS的操作,值得注意的是,compare和delete的必须是同一行数据。
BatchOperations(批量操作)
这里的批量操作和上文介绍的批量操作很类似,但是这里是最泛化的批量操作
- void batch(List<Row> actions)
可以看出,不论是Put,还是Get都是Row的子类。
值得注意的是,在批量操作中,客户端缓冲区将是不会被使用的。
行锁(Row Lock)
在上文的API中,都有可选的rowLock参数,这是显式的行锁,当业务需要强一致性的时候可以考虑使用,显式的行锁可以保证一个插入请求成为一个互斥的独占的请求。当一行数据被锁住的时候,其他的请求都将被阻塞,直到行锁被释放或者阻塞超(这个超时时间可以再配置文件中配置)。Hbase其实也提供有一个隐式的行锁,如果请求没有提供显式的行锁,Hbase将启用隐式的行锁,保证插入操作的原子性。
另外,读取数据是不需要锁的,所以写时的锁不会影响读的性能。
Scan方法
除了Get方法外,Hbase提供查找功能更加强大的Scan方法。Get是根据指定的row key来查找对应的数据,而Scan方法则是根据一系列的条件来查找。
Scan()
- Scan(byte[] startKey, Filter filter)
- Scan(byte[] startKey, byte[] endKey)
- Scan(byte[] startKey)
查找的范围仍然是根据主键来确定, 不带参数的Scan表示将查找整个表,而指定了start和end的Scan则只扫描开始和结束范围内的数据。 Filter参数则可以定义更加复杂的查询条件,满足查询需求,这部分以后再讨论。
Scan中也提供像Get一样的简单的通过指定簇名和列名等条件来查找,但是更多的用法是定义自己的Filter来查找。所以这里不对Scan的简单操作作过多纠缠。
Scan的返回结果为ResultScaner,客户端可以把ResultScaner简单的看作一个Result的集合,但是实际上,ResultScaner比Result要复杂很多。由于Scan是大规模的查找,所以其返回的结果可以也是一个很大的数据集,所以如果让这样大的一个数据集一次性返回给客户端,将给内存和网络造成很大的负担,所以ResultScaner作为一个返回结果,实际上持有一个RPC连接,使客户端在处理结果集的时候可以一批一批的处理,而不是一次性将所有返回结果接受。
- Result next()
- Result[] next(int nbRows)
- void clos()
Hbase提供了两个机制来条件ResultScaner的性能。首先是Scaner Caching缓存(可以在配置文件中配置),这里的缓存定义了一次RPC返回客户端的行数。定义一个更大的缓存数无疑可以提高客户端遍历的性能,但是也有坏处:这将加大客户端内存的压力,并且每一次RPC调用花的时间会更长,因为要使用网络来传输更多的数据。Scaner有一个超时机制,一次RPC用的时间超过了这个阀值的时候,客户端就会抛出一个异常。所以应该根据实际情况来定义超时阀值和缓存数。另外一个机制叫Scaner Batch批次, 缓存是从行的角度来定义一次RPC返回的数据,但是如果返回的结果中包含有具有很多列的行,同样会对内存和网络造成压力,Batch就从列的角度来定义,Batch定义的是一次性最多传输的列的数量。
根据情况合理的设定缓冲和Batch可以使处理查找结果变得高效。