Hbase连接与数据处理

逻辑结构和物理结构

命名空间就相当于于库 命名空间下是表

逻辑结构

image-20230727095514772

物理结构

image-20230727095909737

在建表的时候,我们设置的字段其实是上面说的列族,列我们也可以不用。

启动和停止

先启动Zookeeper和Hadoop

启动Hbase

1
$HBASE_HOME/bin/start-hbase.sh

停止Hbase

1
$HBASE_HOME/bin/stop-hbase.sh

HMaster 的 Web 接口

http://192.168.7.101:16010

http://192.168.7.102:16010

HRegionServer 的 Web 接口

http://192.168.7.101:16030

http://192.168.7.102:16030

http://192.168.7.103:16030

HBase的操作

下面的操作主要是在hbase的shell中操作的,进入hbase shell

1
hbase shell

命名空间

注意

HBase的命名空间 表 列都是区分大小写的。

查看所有namespace

1
list_namespace

创建namespace

1
create_namespace 'ZDB'

删除namespace

1
drop_namespace 'ZDB'

注意

删除命名空间前要删除命名空间下的所有表

删除命名空间下的表

1
2
disable 'ZDB:tuser'
drop 'ZDB:tuser'

查看namespace

1
describe_namespace 'ZDB'

在namespace下创建表

1
create 'ZDB:tuser','s_name','s_sex','s_age','s_dept','s_course'

查看namespace下的表

1
list_namespace_tables 'ZDB'

表操作

创建表

1
2
3
4
create 'student','s_name','s_sex','s_age','s_dept','s_course'

# 创建表示指定保存的版本数
create 'teacher',{NAME=>'username',VERSIONS=>5}

创建命令空间下的表

1
create 'ZDB:tuser','s_name','s_sex','s_age','s_dept','s_course'

查看表详情

1
2
3
describe 'student'

describe 'ZDB:tuser'

显示所有的表

1
list

查看表数据

1
2
3
scan 'student', {LIMIT => 1}
scan 'student', {FORMATTER => 'toString'}
scan 'ZDB:tuser', {FORMATTER => 'toString'}

插入数据

1
2
3
4
5
6
7
8
9
put 'student','95001','s_name','LiYing'
put 'student','95001','s_sex','Male'
put 'student','95001','s_course:math','80'
put 'student','95001','s_course:english','90'

put 'student','95002','s_name','XiaoHong'
put 'student','95002','s_sex','Femal'
put 'student','95002','s_course:math','90'
put 'student','95002','s_course:english','70'

注意:一次只能为一个表的一行数据的一个列,也就是一个单元格添加一个数据,所以直接用shell命令插入数据效率很低,在实际应用中,一般都是利用编程操作数据。

当运行命令:put ‘student’,’95001’,’s_name’,’LiYing’时,即为student表添加了学号为95001,名字为LiYing的一行数据,其行键为95001。

查询数据 HBase中有两个用于查看数据的命令:

  1. get命令,用于查看表的某一行数据;
  2. scan命令用于查看某个表的全部数据

示例

1
2
3
4
5
6
7
get 'student','95001'
get 'student','95001','s_course'
get 'student','95001','s_course:math'

scan 'student'

scan 'student', {FORMATTER => 'toString'}

删除数据 在HBase中用delete以及deleteall命令进行删除数据操作,它们的区别是: ① delete用于删除一个数据,是put的反向操作; ② deleteall操作用于删除一行数据。

1
2
delete 'student','95001','s_sex'
deleteall 'student','95001'

修改数据 在添加数据时,HBase会自动为添加的数据添加一个时间戳,故在需要修改数据时,只需直接添加数据,HBase即会生成一个新的版本,从而完成“改”操作,旧的版本依旧保留,系统会定时回收垃圾数据,只留下最新的几个版本,保存的版本数可以在创建表的时候指定。下面是一个操作的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hbase(main):034:0> get 'student','95001'
COLUMN CELL
s_name: timestamp=1537497681798, value=LiYing
s_sex: timestamp=1537497682400, value=Male
s_course:english timestamp=1537497872225, value=90
s_course:math timestamp=1537497681859, value=80
4 row(s) in 0.0310 seconds

hbase(main):035:0> put 'student','95001','s_course:english','100'
0 row(s) in 0.0130 seconds

hbase(main):036:0> get 'student','95001'
COLUMN CELL
s_name: timestamp=1537497681798, value=LiYing
s_sex: timestamp=1537497682400, value=Male
s_course:english timestamp=1537498062541, value=100
s_course:math timestamp=1537497681859, value=80
4 row(s) in 0.0130 seconds

删除表 删除表有两步,第一步先让该表不可用,第二步删除表。直接drop未disable的表会失败。

1
2
disable 'student'
drop 'student'

查询历史的表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
create 'teacher',{NAME=>'username',VERSIONS=>5}

put 'teacher','91001','username','Mary'
put 'teacher','91001','username','Mary1'
put 'teacher','91001','username','Mary2'
put 'teacher','91001','username','Mary3'
put 'teacher','91001','username','Mary4'
put 'teacher','91001','username','Mary5'

get 'teacher','91001',{COLUMN=>'username',VERSIONS=>5}

hbase(main):064:0> get 'teacher','91001',{COLUMN=>'username',VERSIONS=>5}
COLUMN CELL
username: timestamp=1537498459746, value=Mary5
username: timestamp=1537498455244, value=Mary4
username: timestamp=1537498455193, value=Mary3
username: timestamp=1537498455174, value=Mary2
username: timestamp=1537498455149, value=Mary1
5 row(s) in 0.0110 seconds

退出hbase

1
exit

授权

具备Create权限的namespace Admin可以对表创建和删除、生成和恢复快照

具备Admin权限的namespace Admin可以对表splits或major compactions

授权命名空间

授权zuser01用户对ZDB下的写权限

1
grant 'zuser01' 'W' '@ZDB'

回收zuser01用户对ZDB的所有权限

1
revoke 'zuser01''@ZDB'

当前用户:hbase

1
2
namespace_create 'ns_hbase' 
grant 'zuser02', 'W', '@ns_hbase'

授权表

当前用户:zuser02

1
2
create 'ns_hbase.tuser01', 'name' 
create 'ns_hbase.tuser02', 'name'

zuser02创建了两张表tuser01和tuser02,同时成为这两张表的owner,意味着有RWXCA权限

此时,zuser02团队的另一名成员zuser03也需要获得ns_hbase下的权限,hbase管理员操作如下

如果希望zuser03可以访问已经存在的表,则hbase管理员操作如下

当前用户:hbase

1
2
grant 'zuser03', 'RW', 'ns_hbase.tuser01' 
grant 'zuser03', 'RW', 'ns_hbase.tuser02'

不要只设置写权限

当前用户:hbase

1
grant 'zuser03', 'W', '@ns_hbase'

此时zuser03可以在ns_hbase下创建表,但是无法读、写、修改和删除ns_hbase下已存在的表

当前用户:zuser03

1
scan 'ns_hbase:tuser01'

报错AccessDeniedException

在HBase中启用授权机制

hbase-site.xml

1
2
3
4
5
6
7
8
9
10
11
12
<property>  
<name>hbase.security.authorization</name>
<value>true</value>
</property>
<property>
<name>hbase.coprocessor.master.classes</name>
<value>org.apache.hadoop.hbase.security.access.AccessController</value>
</property>
<property>
<name>hbase.coprocessor.region.classes</name>
<value>org.apache.hadoop.hbase.security.token.TokenProvider,org.apache.hadoop.hbase.security.access.AccessController</value>
</property>

配置完成后需要重启HBase集群

Java操作

添加依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.1.10</version>
</dependency>

Java代码

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package cn.psvmc;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;

import java.io.IOException;
import java.net.URISyntaxException;

public class HbaseTest {
public static Connection getConn() throws IOException, URISyntaxException {

Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.property.clientPort", "2181");
conf.set("hbase.zookeeper.quorum", "hadoop01:2181,hadoop02:2181,hadoop03:2181");
/*
conf.addResource(new Path(ClassLoader.getSystemResource("hbase-site.xml").toURI()));
conf.addResource(new Path(ClassLoader.getSystemResource("core-site.xml").toURI()));
*/
return ConnectionFactory.createConnection(conf);
}

public static void close(Connection conn) throws IOException {
if (null != conn)
conn.close();
}

public static Table getTable(Connection conn, String tablename, String namespace) throws IOException {
TableName tableName1 = TableName.valueOf(namespace, tablename);
return conn.getTable(tableName1);
}

/**
* 添加值
* @param conn 连接
* @param tablename 表名
* @param namespace 命名空间
* @param rowkey 行键
* @param cf 列簇
* @param lie 列标识
* @param value 值
* @throws IOException
*/
public static void put(Connection conn, String tablename, String namespace, String rowkey, String cf, String lie, String value) throws IOException {
Table table = getTable(conn, tablename, namespace);
Put put = new Put(Bytes.toBytes(rowkey));
put.addColumn(Bytes.toBytes(cf), Bytes.toBytes(lie), Bytes.toBytes(value));
table.put(put);
table.close();
}

/**
* 获取数据
* @param conn 连接
* @param tablename 表名
* @param namespace 命名空间
* @param rowkey 行键
* @throws IOException
*/
public static void get(Connection conn, String tablename, String namespace, String rowkey) throws IOException {
Table table = getTable(conn, tablename, namespace);
Get get = new Get(Bytes.toBytes(rowkey));
Result result = table.get(get);
if (result != null) {
Cell[] cells = result.rawCells();
for (Cell cell : cells) {
String row = Bytes.toString(CellUtil.cloneRow(cell));
String family = Bytes.toString(CellUtil.cloneFamily(cell));
String qualifier = Bytes.toString(CellUtil.cloneQualifier(cell));
String value = Bytes.toString(CellUtil.cloneValue(cell));
System.out.printf("行:%s 列族: %s 列名: %s 值: %s%n", row, family, qualifier, value);
}
}
table.close();
}

/**
* 获取数据
* @param conn 连接
* @param tablename 表名
* @param rowkey 行键
* @throws IOException
*/
public static void get(Connection conn, String tablename, String rowkey) throws IOException {
get(conn,tablename,null,rowkey);
}

public static void main(String[] args) throws IOException, URISyntaxException {
Connection conn = HbaseTest.getConn();
HbaseTest.put(conn,"student","default","95001","s_course","yw","100");
HbaseTest.put(conn,"student","default","95001","s_name","zw","小明");
HbaseTest.get(conn,"student","default","95001");
}
}

分页

RowKey + Filter的方式

RowKey一般是必不可少的,但是如果数据量少,几十万数据,就问题不大。很多时候查询都会选择时间,如果能把时间放在RowKey里面,会极大的提升查询的效率。这里有个小技巧:如果Rowkey是按时间戳的方式递增,不要将时间放在二进制码的前面,建议将Rowkey的高位作为散列字段,由程序循环生成,低位放时间字段,这样将提高数据均衡分布在每个Regionserver实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息将产生所有新数据都在一个RegionServer上堆积的热点现象,这样在做数据检索的时候负载将会集中在个别RegionServer,降低查询效率。

HBase的Scan可以通过setFilter方法添加过滤器(Filter),这也是分页、多条件查询的基础。HBase为筛选数据提供了一组过滤器,通过这个过滤器可以在HBase中的数据的多个维度(行,列,数据版本)上进行对数据的筛选操作。通常来说,通过行键,值来筛选数据的应用场景较多。这里简单举个例子,使用SingleColumnValueFilter过滤行,查找数据库中vehicle_speed列是77的数据:

1
2
3
4
5
6
7
8
9
10
FilterList filterList = new FilterList();
SingleColumnValueFilter scvf = new SingleColumnValueFilter(
Bytes.toBytes("f"),
Bytes.toBytes("vehicle_speed"),
CompareOp.EQUAL,
Bytes.toBytes("77")
);
filterList.addFilter(scvf);
scan.setFilter(filterList);
ResultScanner scanner = table.getScanner(scan);

Filter是可以加多个的,HBase提供十多种Filter类型。filterList.addFilter(scvf) 就是可以添加多个查询条件,然后调用setFilter函数给Scanner。

这里再简单介绍一下分页的方式:

  • client分页,scan查到N*M条,过滤掉N*M-M条,返回M条。对于M,N较小时比较适合。
  • 自定义Filter,该filter可以传递offset(server端需要过滤的记录条数),在server端分页,注意,跨不同的region时需要重新计算该offset
  • 缓存上次分页查询的最后一条,下次分页查询从这条(不包含)开始查。
  • 查询条件固定的话,定时任务汇总表
  • PageFilter

使用RowKey + Filter的方式只能满足一些查询(数据量少,或者RowKey是必须的参数),包括其分页的实现并不是最优,但这是使用原生的HBase的方法,比较简单。下面介绍的方法更好,但是依赖于其他的组件。

Phoenix

最早由Salesforce.com开源的Apache Phoenix 是一个Java中间层,可以让开发者在Apache HBase上执行SQL查询,目前的版本基本支持常用的操作(分页,排序,Group By,Having,函数,序列等等)。目前的Phoenix是非常成熟的解决方案,阿里、Salesforce、eBay等互联网都在广泛使用。

Phoenix完全使用Java编写,代码位于GitHub上,并且提供了一个客户端可嵌入的JDBC驱动。它查询的实时性非常高,一般查询都在秒级返回,可以应用OLTP的系统中。在用户必须通过Phoenix来建HBase的表,它会映射到HBase的表上。Phoenix可以创建索引来提升提升多条件查询HBase的效率。比如,在查询订单的时候,可以通过订单号、时间、状态等不同的维度来查询,要想把这么多角度的数据都放到RowKey中几乎不可能。而在Phoenix中,你可以针对这几个字段建立索引。在写SQL语句的时候,如果Where语句中使用到了这些条件,Phoenix就会自动判断是否走索引。

Phoenix的索引本质上也是一张HBase的表,它维护了索引和RowKey的关系。在查询的时候,它会从索引表中先找到RowKey,然后再根据RowKey再去HBase原始数据表中获取数据。

ElasticSearch/Solr + HBase

针对HBase使用RowKey访问超高的效率,我们可以把索引数据放在类似于ElasticSearch或者Solr这样的搜索引擎里面。用搜索引擎做二级索引。查询数据的时候先从搜索引擎中查询出RowKey,然后再用RowKey去获取数据。流行的搜索引擎基本可以满足查询的所有需求。

举个例子:

订单数据项有10个,但是用于查询的有5个。当数据插入HBase的同时,也把这5个数据项加上预先生成的RowKey插入搜索引擎,也就是说部分数据存储两份。

image-20230128140440881

选择

  • 当数据量较小几十万,上百万的话可以使用RowKey+Filter的方式实现。

  • 如果数据量到了千万,甚至亿级别,可以尝试Phoenix。

  • 如果数据量到了10亿或者更多则需要选择搜索引擎。

同时方案的系统维护难度和对技术的要求也是逐级递增的。