数据库的初始基本操作被称CURD(Create,Read,Update,Delete),具体指增、查、改、删。HBase中有与之对应的一组操作。
下面介绍的这些组操作可以被分为两类:一类操作用于单行,另一类操作用于多行。鉴于后面有一些内容比较复杂,我们将分开介绍着两类操作。同时,我们还会介绍一些衍生的客户端的API特性。
单行put
也许你现在最想了解的就是如何向HBase中存储数据,下面就是实现这个功能的调用:
void put(Put put) throws IOException
这个方法以单个Put或存储在列表中的一组Put对象作为输入参数,其中Put对象是由以下几个构造函数创建的:
Put(byte[] row)
Put(byte[] row,RowLock rowLock)
Put(byte[] row,long ts)
Put(byte[] row,long ts,RowLock rowLock)
创建Put实例时用户需要提供一个行键row,在HBase中每行数据都有唯一的行键(row key)作为标识,跟HBase的大多数数据类型一样,它是一个Java的byte[]数组。用户可以按自己的需求来指定每行的行键。现在假设用户可以随意设置行键,通常情况下,行键的含义与真实场景相关,例如它的含义可以是一个用户名或者订单号,它的内容可以是简单的数字,也可以是较复杂的UUID(全局统一标识符)等。
HBase 非常友好地为用户提供了一个包含很多静态方法的辅助类,这个类可以把许多Java数据类型转换为byte[]数组。
Bytes类所提供的方法
static byte[] toBytes(ByteBuffer bb)
static byte[] toBytes(String s)
static byte[] toBytes(boolean b)
static byte[] toBytes(long val)
static byte[] toBytes(float f)
static byte[] toBytes(int val)
创建Put实例后,就可以向该实例添加数据了,添加数据的方法如下:
Put add(byte[] family,byte[] qualifier,byte[] value)
Put add(byte[] family,byte[] qualifier,long ts,byte[] value)
Put add(KeyValue kv) throws IOException
每一次调用add()都可以特定地添加一行数据,如果再加一个时间戳选项,就能成为一个数据单元格。注意,当不指定时间戳调用add()方法时,Put实例会使用来自构造函数的可选时间戳参数(也称作ts),如果用户在构造Put实例是也没有指定时间戳,则时间戳将会有region服务器设定。
系统为一些高级用户提供了KeyValue实例的变种,这里说的高级用户是指知道怎样检索或创建这个内部类的用户。KeyValue实例代表了一个唯一的数据单元格,类似于一个协调系统,该系统使用行键、列族、列限定符、时间戳指向一个单元格的值,像一个三维立方体系统。
获取Put实例内部添加的KeyValue实例需要调用与add()相反的方法get():
List<KeyValue> get(byte[] family,byte [] qualifier)
Map<byte[],List<KeyValue>> getFamilyMap()
以上两个方法可以查询用户之间添加的内容,同时将特定单元格的信息转换成KeyValue实例。用户可以选择获取整个列族的全部数据单元,一个列族中的特定列或者是全部数据。后面的getFamilyMap()方法可以遍历Put实例中每一个可用的KeyValue实例,检查其中包含的详细信息。
用户可以采用以下这些方法检查是否存在特定单元格,而不需要遍历整个集合:
boolean has(byte[] family,byte[] qualifier)
boolean has(byte[] family,byte[] qualifier,long ts)
boolean has(byte[] family,byte[] qualifier,byte[] value)
boolean has(byte[] family,byte[] qualifier,long ts,byte[] value)
随着以上方法所使用参数的逐步细化,获得的信息也越详细,当找到匹配的列时返回的对象为true。第一个方法仅检查一个列是否存在,其他的方法则增加了检查时间戳、限定值的选项。
Put类还提供了很多其他方法,如下图所示
方法 | 描述 |
---|---|
getRow() | 返回创建Put实例时所指定的行键 |
getRowLock() | 返回当前Put实例的行RowLock实例 |
getLockId() | 返回使用rowlock参数传递给构造函数的可选的锁ID,当未被指定时返回-1L |
setWriteToWAL() | 允许关闭默认启用的服务端预写日志(WAL)功能 |
getWriteToWAL() | 返回代表是否启用了WAL功能 |
getTimeStamp() | 返回相应Put实例的时间戳,该值可在构造函数中由ts参数传入,当未被设定时返回Long.MAX_VALUE |
heapSize() | 计算当前Put实例所需的堆大小,既包含其中的数据,也包含内部数据结构所需的空间 |
isEmpty() | 检查FamilyMap是否含有任何KeyValue实例 |
numFamilies() | 查询FamilyMap的大小,即所有的KeyValue实例中列族的个数 |
size() | 返回本次Put会添加的KeyValue的实例 |
代码如下
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.util.Bytes;
public class PutExample {
public static void main(String[] args) throws IOException {
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "testtable");
Put put = new Put(Bytes.toBytes("row1"));
put.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"),
Bytes.toBytes("val1"));
put.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"),
Bytes.toBytes("val2"));
table.put(put);
}
}
进入hbase安装目录下的bin目录,输入./start-hbase.sh
启动hbase,然后输入 hbase shell
,验证是否插入成功。
hbase(main):001:0>list
TABLE
testtable
1 row(s) in 0.0400 seconds
hbase(main):002:0>scan 'testtable'
ROW COLUMN+CELL
row1 column=colfam1:qual1,timestamp=1294065304642,value=val1
1 row(s) in 0.2050 seconds
在创建Put实例时用到的一个可选参数是ts,即时间戳。在HBase表中,时间戳使用户可以在HBase中将数据存储为一个特定版本
KeyValue 类
在代码中有时需要直接处理KeyValue实例。它们都含有一个特定单元格的数据以及坐标。坐标包括行键、列族名、列限定符以及时间戳。该类提供了特别多的构造器,允许以各种方式组合这些参数。下面展示了包括所有参数的构造器:
KeyValue(byte[] row, int roffset, int rlength, byte[] family, int foffset, i
nt flength, byte [] qualifier,
int qlength, long timestamp,
Type type, byte[] value, int voffset, int vlength)
数据和坐标都是以Java的byte[]形式存储的,即以字节数组的形式存储的。使用这种底层存储类型的目的是,允许存储任意类型的数据,并且可以有效地只存储所需的字节,这保证了最少的内部数据结构开销。另一个原因是,每一个字节数组都有一个offset参数和一个lenght参数,它们允许用户提交一个已存在的字节数组,并进行效率很高的字节级别的操作。
坐标中任意一个成员都有一个getter方法,可以获得字节数组以及它们的参数offset和length。不过也可以在最顶层访问它们,即直接读取底层字节缓冲区:
byte [] getBuffer()
int getOffset()
int getLength()
它们返回当前KeyValue实例中字节数组完整信息。用户用到这些方法的场景很少,但是在需要的时候,还是可以使用这些方法的。
还有两个有意思的方法:
byte [] getRow()
byte [] getKey()
读者也许会问这样一个问题:行(row)和键(key)有什么区别?行目前来说指的是行键,即Put构造器里的row参数。键是一个单元格的坐标,用的是原始的字节数组格式。在实践中,几乎用不到getKey(),但有可能用到getRow()。
KeyValue类还提供了一系列实现了Comparator接口的内部类,可以在代码里使用它们来实现与HBase内部一样的比较器。当需要使用API获取KeyValue实例时,并进一步排序或按照顺序处理时,就要用到这些比较器。如下表所示,KeyValue提供的比较器的简要概述
比较器 | 描述 |
---|---|
KeyComparator | 比较两个KeyValue实例的字节数组格式的行键,即getKey()方法的返回值 |
KVComparator | 是KeyComparator的封装,基于两个给定的KeyValue实例,提供与KeyComparator一样的功能 |
RowComparator | 比较两个KeyValue实例的行键(getRow()的返回值) |
MetaKeyComparator | 比较两个以字节数组格式表示的.META.条目的行键 |
MetaComparator | KVComparator类的一个特别版本,用于比较.META.目录中的条目,是MetaKeyComparator的封装 |
RootKeyComparator | 比较两个字节数组格式表示的-ROOT-条目的行键 |
RootCompartor | KVCompartor类的一个特别版本,用于比较-ROOT-目录表中的条目,是RootKeyComparator的封装 |
KeyValue类将大部分的比较器按照静态实例提供给其他类使用所以可以不用创建自己的实例,二是使用提供的实例。例如,可以按照以下方法创建一个KeyValue实例的集合,这个集合可以按照HBase内部使用的顺序来排序:
TreeSet<KeyValue> set=new TreeSet<KeyValue>(KeyValue.COMPARATOR)
KeyValue实例还有一个变量(一个额外的属性),代表该实例的唯一坐标,下表列出了所有可能的值
类型 | 描述 |
---|---|
Put | KeyValue实例代表一个普通的Put操作 |
Delete | KeyValue实例代表一个Delete操作,也称为墓碑标记 |
DeleteColumn | 与Delete相同,但是会删除一整列 |
DeleteFamily | 与Delete相同,但是会删除整个列族,包括该列族的所有列 |
可以通过使用另外一个方法来查看一个KeyValue实例的类型,例如:String toString()
该方法也会按照以下格式打印出当前KeyValue实例的元信息:
<row-key>/<family>:<qualifier>/<version>/<type>/<value-length>
该类有很多更便捷的方法:允许对存储数据的其中一部分进行比较,检查实例的类型是什么,获得它已经计算好的堆大小,克隆或者复制该类等。有一些静态方法可以创建一些特殊的KeyValue实例,用以在HBase内更底层地比较或操作数据。
客户端的写缓冲区
HBase的API配备了一个客户端的写缓冲区,缓冲区负责收集put操作,然后调用RPC操作一次性将put送往服务器。全局交换机控制着该缓冲区是否在使用,以下是其方法:
void setAutoFlush(boolean autoFlush)
boolean isAutoFlush()
默认情况下,客户端缓冲区是禁用的。可以通过将自动刷写设置为false来激活缓冲区,调用如下:
table.setAutoFlush(false)
启用客户端缓冲机制后,用户可以通知isAutoFlush()方法检查标识的状态。当用户初始化创建一个HTable实例是,这个方法将返回true,如果有用户修改过缓冲机制,它会返回用户当前所设定的状态。
激活客户端缓冲区之后,将数据存储到HBase中。此时的操作不会产生RPC调用,因为存储的Put实例保存在客户端进程的内存中。当需要强制把数据写到服务端是,可以调用另外一个API函数:
void flushCommits() throws IOException
flushCommit()方法将所有的修改传送到远程服务器。被缓冲的Put实例可以跨多行。客户端能够批量处理这些更新,并把它们传到到对应的region服务器。和调用单行put()方法一样,用户不需要担心数据分配到了哪里,因为对于用户来说,HBase客户端对这个方法的处理时透明的,下图是客户端put操作按所属region服务器排序和分组
缓冲区仅在以下两种情况下会刷写。
- 显式刷写
用户调用flushCommit()方法,把数据发送到服务器做永久存储
- 隐式刷写
隐式刷写会在用户调用put()或setWriteBufferSize()方法时触发。这两个方法都会将目前占用的缓冲区大小与用户配置的大小做比较,如果超出限制则会调用flushCommits()方法。如果缓冲区被禁用,可以设置setAutoFlush(true),这样用户每次调用put()方法时都会触发刷写。
客官请看代码
public class HTableFlush {
public static void main(String[] args) throws IOException {
Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "testtable");
System.out.println("Auto flush :" + table.isAutoFlush());
table.setAutoFlush(false);
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"),
Bytes.toBytes("val1"));
table.put(put1);
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"),
Bytes.toBytes("val2"));
table.put(put2);
Put put3 = new Put(Bytes.toBytes("row3"));
put3.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual3"),
Bytes.toBytes("val3"));
table.put(put3);
Get get = new Get(Bytes.toBytes("row1"));
Result res1 = table.get(get);
System.out.println("Result:" + res1);
table.flushCommits();
Result res2 = table.get(get);
System.out.println("Result :" + res2);
}
}
Put列表
客户端的API可以插入单个Put实例,同时也有批量处理操作的高级特性。
代码如下
List<Put> puts = new ArrayList<Put>();
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"),
Bytes.toBytes("val1"));
puts.add(put1);
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"),
Bytes.toBytes("val2"));
puts.add(put2);
Put put3 = new Put(Bytes.toBytes("row3"));
put3.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual3"),
Bytes.toBytes("val3"));
puts.add(put3);
table.put(puts);
服务器遍历所有的操作并设法执行它们,失败的会返回,然后客户端会使用RetriesExhaustedWithDetailsException报告远程错误,这样用户可以查询有多个操作失败、出错的原因以及重试的次数
向HBase中插入一个错误列表
List<Put> puts = new ArrayList<Put>();
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"),
Bytes.toBytes("val1"));
puts.add(put1);
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("BOGUS"), Bytes.toBytes("qual1"),
Bytes.toBytes("val2"));
puts.add(put2);
Put put3 = new Put(Bytes.toBytes("row2"));
put3.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"),
Bytes.toBytes("val3"));
puts.add(put3);
table.put(puts);
原子性操作 compare-and-set
这是一种特别的put调用,其能保证自身操作原子性:检查写(check and put)。该方法签名如下:
boolean checkAndPut(byte[] row,byte[] family,byte[] qualifier,byte[] value,Put put) throws IOException
这种带有检查功能的方法,就能保证服务端put操作的原子性。如果检查成功通过,就执行put操作,否则就彻底放弃修改操作。
代码如下
Put put1 = new Put(Bytes.toBytes("row1"));
put1.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"),
Bytes.toBytes("val1"));
boolean res1 = table.checkAndPut(Bytes.toBytes("row1"),
Bytes.toBytes("qual1"), Bytes.toBytes("val1"), null, put1);
System.out.println("Put applied: " + res1);
table.put(put1);
boolean res2 = table.checkAndPut(Bytes.toBytes("row1"),
Bytes.toBytes("qual1"), Bytes.toBytes("val1"), null, put1);
System.out.println("Put applied: " + res2);
Put put2 = new Put(Bytes.toBytes("row2"));
put2.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual2"),
Bytes.toBytes("val2"));
boolean res3 = table.checkAndPut(Bytes.toBytes("row1"),
Bytes.toBytes("qual1"), Bytes.toBytes("val1"), null, put2);
System.out.println("Put applied: " + res3);
table.put(put2);
Put put3 = new Put(Bytes.toBytes("row3"));
put3.add(Bytes.toBytes("colfam1"), Bytes.toBytes("qual3"),
Bytes.toBytes("val3"));
boolean res4 = table.checkAndPut(Bytes.toBytes("row1"),
Bytes.toBytes("qual1"), Bytes.toBytes("val1"), null, put3);
System.out.println("Put applied: " + res4);