wangbo123855842 / Learning

15 stars 2 forks source link

HBase #45

Open wangbo123855842 opened 4 years ago

wangbo123855842 commented 4 years ago
スクリーンショット 2020-11-03 12 42 08

HBase

HBase 是一个分布式的非关系型数据库。它利用 Hadoop 分布式文件系统 HDFS 提供分布式数据存储。 它不支持 SQL 的跨行事务,也不要求数据之间有严格的关系,同时它允许在同一列的不同行中存储不同类型的数据。

HBase 的特点

HBase 的单表可以有百亿行、百万列。 HBase 采用 LSM 树作为内部数据存储结构,这种结构会周期性地将较小文件合并成大文件,以减少对磁盘的访问。

大部分传统的关系型数据库,都是面向行来组织数据的。如 Mysql,Postgresql。 HBase 是采用列存储的。 行存储和列存储,是数据库底层组织数据的方式

比如,这样的一套数据

[
  {
    "title": "Oriented Column Store",
    "author": "Alex",
    "publish_time": 1508423456,
    "like_num": 1024
  },{
    "title": "Apache Druid",
    "author": "Bob",
    "publish_time": 1504423069,
    "like_num": 10
  },{
    "title": "Algorithm",
    "author": "Casey",
    "publish_time": 1512523069,
    "like_num": 16
  }
]

行存储的方式

スクリーンショット 2020-11-06 22 56 08

行存储将会以上面方式将数据存储在磁盘上。它利于数据一行一行的写入,写入一条数据记录时,只需要将数据追加到已有数据记录后面即可。所以数据的写入会更快。对按记录查询数据也更简单。

但是,大量数据集来聚合统计数据时候,首先需要将所有行数据读入内存,在对某个字段集计。

スクリーンショット 2020-11-06 22 59 44

列存储的方式

スクリーンショット 2020-11-06 23 00 49

列存储的聚合集计

スクリーンショット 2020-11-06 23 01 41

因为基于列存储,所以每一列本身就相当于索引。所以在做一些需要索引的操作时,就不需要额外的数据结构来为此列创建合适的索引。

通常在传统的关系性数据库中,每一列的数据类型是事先定义好的,会占用固定的内存空间,在此情况下,属性值为空(NULL)的列也需要占用存储空间。 而在 HBase 中的数据都是以字符串形式存储的,为空的列并不占用存储空间,因此 HBase 的列存储解决了数据稀疏性的问题,在很大程度上节省了存储开销。

HBase 工作在 HDFS 之上,也继承了 HDFS 的可扩展性。 HBase 的扩展是横向的,横向扩展是指在扩展时不需要提升服务器本身的性能,只需添加服务器到现有集群即可。

HBase 表根据 Region 大小进行分区,分别存在集群中不同的节点上,当添加新的节点时,集群就重新调整,在新的节点启动 HBase 服务器,动态地实现扩展。 这里需要指出,HBase 的扩展是热扩展,即在不停止现有服务的前提下,可以随时添加或者减少节点。

HBase 运行在 HDFS 上,HDFS 的多副本存储可以让它在岀现故障时自动恢复,同时 HBase 内部也提供 WAL 和 Replication 机制

WAL(Write-Ahead-Log)预写日志是在 HBase 服务器处理数据插入和删除的过程中用来记录操作内容的日志,保证了数据写入时不会因集群异常而导致写入数据的丢失;而 Replication 机制是基于日志操作来做数据同步的

当集群中单个节点出现故障时,协调服务组件 ZooKeeper 通知集群的主节点,将故障节点的 HLog 中的日志信息分发到各从节点进行数据恢复

HBase 组件

スクリーンショット 2020-11-06 21 56 28

HBase 的客户端

  1. 包含访问 HBase 的接口,比如,Shell 和 Java API。
  2. 它会维护缓存来加速访问 HBase 的速度,比如 Region 的位置。
  1. 监听 HMaster 的状态,保证有且仅有一个活跃的 HMaster,实现高可用。
  2. 存储所有 Region 的寻址入口,比如 root 表存放在哪个服务器上。
  3. 实时监控 HRegionServer 的状态,并实时通知给 HMaster。
  4. 存储 HBase 的部分元信息。
  1. 为 HRegionServer 分配 Region。比如新建表。
  2. 负责 HRegionServer 的负载均衡。
  3. 负责 Region 的重新分配,比如,发现失效的 HRegionServer,并重新分配它的 Region 到其他 HRegionServer 工作。
  4. 管理用户对表的增删改查(只负责请求传递)
  1. 维护 HMaster 分配的 Region,也就是管理本机的 Region。
  2. 处理 Client 对 Region 的的读写请求,做真正的增删改查操作。并与 HDFS 交互。
  3. 水平切分运行中变得过大的 Region (裂变,尽量等分)。

HBase 分布式存储和负载均衡的最小单元,它是表或者表的一部分

一个 Region 由多个 Store 组成,每一个 Store 对应一个 ColumnFamily (列簇), Store 包含 MemStore 和 StoreFile。

内存缓存区,数据的写操作会先写到 MemStore 中,当 MemStore 中的数增长到一个阈值后,RegionServer 会启动 flashcatch 进程写入到 StoreFile 中,每次写入形成一个单独的 StoreFile。 默认大小是 128M。

和 HFile 是同一个东西,只不过是站在 HDFS 角度称这个文件为 Hfile,在 Hbase 角度就叫 StoreFile。 客户端检索数据,现在 MemStore 找,找不到再找 storeFile。

当一个 Region 所有的 StoreFile 大小和数量增长到超过一定阈值后,会把当前 Regin 分割为两个, 并且由 HMaster 分配到相应的 RegionServer 服务器,实现负载均衡

对 HBase 的操作进行记录,先写进日志,再写进 MemStore,防止数据丢失

HBase 数据模型

Base 是一种列存储模式与键值对存储模式结合的 NoSQL 数据库。 HBase 可以实现自动的数据分片,用户不需要知道数据存储在哪个节点上,只要说明检索的要求,系统会自动进行数据的查询。 HBase 也是以表的方式组织数据,应用程序将数据存于 HBase 的表中,HBase 的表也由行和列组成。

HBase 有列簇的概念,它将一列或多列组织在一起,HBase 的每个列必须属于某一个列簇

HBase 中的数据以表的形式存储。 同一个表中的数据通常是相关的,使用表主要是可以把某些列组织起来一起访问。 表名作为 HDFS 存储路径的一部分来使用,在 HDFS 中可以看到每个表名都作为独立的目录结构

在 HBase 表里,每一行代表一个数据对象,每一行都以行键(Row Key)来进行唯一标识,行键可以是任意字符串。 在 HBase 内部,行键是不可分割的字节数组,并且行键是按照字典排序由低到高存储在表中的。 在 HBase 中可以针对行键建立索引,提高检索数据的速度。

HBase 中的列簇是一些列的集合,列簇中所有列成员有着相同的前缀,列簇的名字必须是可显示的字符串。 列簇支持动态扩展,用户可以很轻松地添加一个列簇或列,无须预定义列的数量以及类型所有列均以字符串形式存储用户在使用时需要自行进行数据类型转换

列簇中的数据通过列标识来进行定位,列标识也没有特定的数据类型,以二进制字节来存储。 通常以 Column Family:Colunm Qualifier 来确定列簇中的某列。

每一个行键、列簇、列标识共同确定一个单元格,单元格的内容没有特定的数据类型,以二进制字节来存储。 每个单元格保存着同一份数据的多个版本不同时间版本的数据按照时间先后顺序排序,最新的数据排在最前面。 单元格可以用 <RowKey,Column Family: Column Qualifier,Timestamp> 元组来进行访问。

在默认情况下,每一个单元格插入数据时都会用时间戳来进行版本标识。 读取单元格数据时,如果时间戳没有被指定,则默认返回最新的数据。 写入新的单元格数据时,如果没有设置时间戳,默认使用当前时间。 每一个列簇的单元数据的版本数量都被 HBase 单独维护,默认情况下 HBase 保留 3 个版本数据

スクリーンショット 2020-11-07 21 01 34

在 HBase 中,列不是固定的表结构,在创建表时,不需要预先定义列名,可以在插入数据时临时创建关系型数据库中表的结构需要预先定义,如列名及其数据类型和值域等内容。 如果需要添加新列,则需要修改表结构,这会对已有的数据产生很大影响。 同时,关系型数据库中的表为每个列预留了存储空间,即空白 Cell 数据在关系型数据库中以 NULL 值占用存储空间。 因此,对稀疏数据来说,关系型数据库表中就会产生很多 NULL 值,消耗大量的存储空间。 在 HBase 中,如图那样空白 Cell 在物理上是不占用存储空间的,即不会存储空白的键值对。

HBase 安装

  1. 安装 JDK
  2. 安装 Hadoop 集群(HDFS集群)

单机安装

安装 HBase

tar xzvf hbase-1.2.6-bin.tar.gz /usr/local

hbase-env.sh 配置 HBase 运行时的变量,如 Java路径、RegionServer 相关参数等。

export JAVA_HOME=/usr/java/j dkl.8.0_161

单机版的环境,ZooKeeper 可以作为 HBase 的一部分来管理启动,即 ZooKeeper 随着 HBase 的启动而启动,随其关闭而关闭。

export HBASE_MANAGES_ZK = true

ZooKeeper 也可以作为独立的集群来运行,即完全与 HBase 脱离关系,这时需要设置 HBASE_MANAGES_ZK 变量为 false。

hbase-site.xml 它是 HBase 的主要配置文件,在这个文件中可以添加 HBase 的相关配置,如分布式的模式、ZooKeeper 的配置等。

<configuration>
    <property>
        <name> hbase.rootdir </name>
        <value>hdfs://example0:9000/hbase</value>
        <description> hbase.rootdir是RegionServer的共享目录,用于持久化存储HBase数据,默认写入/tmp中。如果不修改此配置,在HBase重启时,数据会丢失。此处一般设置的是hdfs的文件目录,如NameNode运行在namenode.Example.org主机的9090端口,则需要设置为hdfs://namenode.example.org:9000/hbase
        </description>
    </property>
    <property>
        <name>hbase.cluster.distributed</name>
        <value>true</value>
        <description>此项用于配置HBase的部署模式,false表示单机或者伪分布式模式,true表示完全分布式模式。
        </description>
    </property>
    <property>
        <name>hbase.zookeeper.quorum</name>
        <value>examplelz example2,example3</value>
        <description>此项用于配置ZooKeeper集群所在的主机地址。examplel、 example2、example3是运行数据节点的主机地址。   
        </description>
    </property>
    <property>
        <name>hbase.zookeeper.property.dataDir</name>
        <value>/var/zookeeper</value>
        <description>此项用于设置存储ZooKeeper的元数据,如果不设置默认存在/tmp下,重启时数据会丢失。
        </description>
    </property>
</configuration>

全分布式环境安装

比如,三台机器 machine1,machine2,machine3

machine1:Master Active ,ZooKeeper
machine2:Master Backup,RegionServer,ZooKeeper
machine3:RegionServer,ZooKeeper
export HBASE_MANAGES_ZK = false
<!-- 配置 HBase 的数据存放位置 -->
<property>
    <name> hbase.rootdir </name>
    <value>hdfs://example0:9000/hbase</value>
</property>

<!-- 配置 ZooKeeper -->
<property>
    <name> hbase.zookeeper.quorum </name>
    <value>machine1:2181, machine2:2181, machine3:2181</value>
</property>

<!-- 将 HBase 分布式集群功能开启 -->
<property>
    <name>hbase.cluster.distributed</name>
    <value>true</value>
</property>
machine2
machine3
machine2

首先要确认 HDFS 处于运行状态,使用 jps 命令查看 NameNode 和 DataNode 的服务是否正常启动。 之后也要启动 ZooKeeper 服务。 在 Master 服务器上已经配置了对集群中所有 Slave 机器的无密码登录使用 start-hbase.sh 脚本即可启动整个集群

bin/start-hbase.sh

使用 jps 命令查看进程,如果是完全分布式模式,则

  1. 在 Master 节点运行有 Hmaster 和 HQuorumPeer 进程,
  2. 在 Region 节点上运行 HRegionServer 和 HQuorumPeer 进程。
[root@localhost local]# jps
24371 NameNode
24680 SecondaryNameNode
26328 HRegionServer
24506 DataNode
26508 Jps
25455 HQuorumPeer
25519 HMaster

HBase Shell

HBase 数据库默认的客户端程序是 HBase Shell,它是一个命令行工具。 用户可以使用 HBase Shell,通过命令行的方式与 HBase 进行交互。 在 HBase 的 HMaster 主机上通过命令行输入 hbase shell,即可进入 HBase 命令行环境。

DDL NameSpace

在 RDBMS 中有 database 的概念,用来对 table 进行分组,那么在 HBase 中当表比较多的时候如何对表分组呢,就是namespace,可以简单的把 namespace 理解为 RDBMS 中的 database

HBase 有两个内置的 namespace:default 和 hbase

hbase(main):009:0> list_namespace
NAMESPACE
default
hbase
2 row(s)
Took 0.0683 seconds

hbase,用来存放系统相关的一些元数据等。

hbase(main):025:0> list_namespace_tables "hbase""
TABLE
meta
namespace
2 row(s)
Took 0.0174 seconds
=> ["meta", "namespace"]

default,创建表时未指定命名空间的话默认挂在 default 下

hbase(main):010:0> create_namespace "test"
Took 0.2781 seconds

在创建namespace的时候还可以添加一些说明信息

hbase(main):018:0> create_namespace "test002", {"author"=>"CC11001100", "create_time"=>"2018-11-4 17:51:53"}
Took 0.2262 seconds
hbase(main):019:0> describe_namespace "test002""
DESCRIPTION
{NAME => 'test002', author => 'CC11001100', create_time => '2018-11-4 17:51:53'}
Took 0.0042 seconds
=> 1
hbase(main):014:0> alter_namespace "test002", {METHOD=>"set", "author"=>"ChenEr"}
Took 0.2458 seconds
hbase(main):019:0> list_namespace
NAMESPACE
default
hbase
test
hbase(main):020:0> drop_namespace "test002"
Took 0.2321 seconds

DDL Table

namespace 是 ns1,表名是 t1,列簇 f1 , 保留版本 5 个,列簇 f2,列簇 f3

create 'ns1:t1', {NAME => 'f1', VERSIONS => 5}, {NAME => 'f2'}, {NAME => 'f3'}

不指定列簇的其他属性,比如版本,可以使用这样的简写

create 'ns1:t1', 'f1', 'f2', 'f3'

在 Hbase 中,split 是一个很重要的功能,Hbase 是通过把数据分配到一定数量的 region 来达到负载均衡的一个 table 会被分配到一个或多个 region 中,这些 region 会被分配到一个或者多个 regionServer 中在自动 split 策略中,当一个 region 达到一定的大小就会自动 split 成两个 region。 table 在 region 中是按照 row key 来排序的,并且一个 row key 所对应的行只会存储在一个region中,这一点保证了 Hbase 的强一致性 。

我们可以自定义切分点,这样就按照 row Key,分了5个 region。

 create 'ns1:t2', 'f1', SPLITS => ['10', '20', '30', '40']
スクリーンショット 2020-11-13 13 52 06

自动 split

当一个 region 达到一定的大小,他会自动split称两个region。 0.94版本之后,默认的策略是,最小的分裂大小和 table 的某个 region server 的 region 个数有关。 当 store file 的大小大于如下公式得出的值的时候就会split,公式如下

Min (R^2 * “hbase.hregion.memstore.flush.size”, “hbase.hregion.max.filesize”) 

R 为同一个 table 中在同一个 region server 中 region 的个数。

list 'ns1.*'

直接 list 的话就是查询所有命名空间下的所有表。

HBase 表的结构和表的管理可以通过 alter 命令来完成,使用这个命令可以完成更改列族参数信息、增加列族、删除列族以及更改表的相关设置等操作。

修改列簇

alter 'Student', {NAME => 'Grades', VERSIONS => 3}

增加列簇

alter 'Student', 'hobby'

删除列簇

alter 'Student', 'delete' => 'hobby'

HBase 使用 drop 命令删除表,但是在删除表之前需要先使用 disable 命令禁用表

例如有一个 Student 表,删除该表的完整流程如下

disable 'Student'
drop 'Student'

使用 disable 禁用表以后,可以使用 is_disable 查看表是否禁用成功。

另外,如果只是想清空表中的所有数据,使用 truncate 命令即可。

truncate 'Student'

HBase 使用 put 命令向数据表中插入数据,put 向表中增加一个新行数据,或覆盖指定行的数据

スクリーンショット 2020-11-13 19 18 05

例如有以上结构的数据表,向其中插入一条数据的写法为。

put 'Student', '0001', 'Stulnfo:Name', 'Tom Green', 1

第二个参数 0001 为行键的名称,最后一个参数 1 为时间戳,如果不设置时间戳,则系统会自动插入当前时间为时间戳。

put 命令只能插入一个单元格的数据,上表中的一行数据需要通过以下几条命令一起完成。

put 'Student', '0001', 'StuInfo:Name', 'Tom Green', 1
put 'Student', '0001', 'StuInfo:Age', '18'
put 'Student', '0001', 'StuInfo:Sex', 'Male'
put 'Student', '0001', 'Grades:BigData', '80'
put 'Student', '0001', 'Grades:Computer', '90'
put 'Student', '0001', 'Grades:Math', '85'

如果 put 语句中的单元格是已经存在的,即行键、列簇及列名都已经存在,且不考虑时间戳的情况下,执行 put 语句,则可对数据进行更新操作。 如果在初始创建表时,已经设定了列族 VERSIONS 参数值为 n,则 put 操作可以保存 n 个版本数据,即可查询到行键为 0001 的学生的 n 个版本的姓名数据。

delete 命令可以从表中删除一个单元格或一个行集,语法与 put 类似,必须指明表名和列簇名称,而列名和时间戳是可选的。

例如,执行以下命令,将删除 Student 表中行键为 0002 的 Grades 列簇的所有数据。

delete 'Student', '0002', 'Grades'

需要注意的是,delete 操作并不会马上删除数据,只会将对应的数据打上删除标记(tombstone),只有在合并数据时,数据才会被删除。

执行以下命令将删除 Student 表中行键为 0001,Grades 列簇成员为 Math,时间戳小于等于 2 的数据。

delete 'Student', '0001', 'Grades:Math', 2

如需删除表中所有列簇在某一行上的数据,即删除上表中一个逻辑行,则需要使用 deleteall 命令。

deleteall 'Student', '0001'

手工把 MemStore 写到 Hfile 中

flush 't1'

比如,删除所有的数据

deleteall 't1' , 'r7'
flush 't1'

每次 flush 都会建一个新的 hfile。

get 命令可以从数据表中获取某一行记录,类似于关系型数据库中的 select 操作。get 命令必须设置表名和行键名,同时可以选择指明列簇名称、时间戳范围、数据版本等参数。

执行以下命令可以获取 Student 表中行键为 0001 的所有列簇数据

get 'student', '0001'

获取某个列簇的某个字段

get 't1' , 'rowkey001' , 'f1:col1'

scan 命令,用来查询指定命名空间,指定表的全部数据,使用时只需指定命名空间:表名即可。

scan 'n1:t1'

指定列簇,指定列的所有数据

scan 'n1:t1' , {Column => { f1 : id } }

分页

scan 't1', {COLUMNS => ['f1'], LIMIT => 10, STARTROW => '001'} 

按照时间范围来查询

scan n1:t1', {COLUMNS => 'f1', TIMERANGE => [1303668804, 1303668904]} 

查询最后三个版本的数据

scan n1:t1', {RAW => true , VERSIONS => 3} 

统计表的行数

count n1:t1

JAVA HBase

添加依赖

<dependency>
  <groupId>org.apache.hbase</groupId>
  <artifactId>hbase-shaded-client</artifactId>
  <version>2.0.0</version>
</dependency>

连接类

public static Connection getConnection() {
    // 获取配置
    Configuration configuration = getConfiguration();
    // 检查配置
    HBaseAdmin.checkHBaseAvailable(configuration);
    // 创建连接
    return ConnectionFactory.createConnection(configuration);
}

private static Configuration getConfiguration() {
    Configuration config = HBaseConfiguration.create();
    config.set("hbase.zookeeper.quorum", "localhost");  // ZooKeeper 地址
    return config;
}

Namespace 的 CRUD

public void listNamespace() throws IOException {
    // 1. 获取到 namespace 的数组
    NamespaceDescriptor[] namespaceDescriptors = admin.listNamespaceDescriptors();
    // 2. 打印 namespace
    for(NamespaceDescriptor namespaceDescriptor : namespaceDescriptors) {
        System.out.println(namespaceDescriptor.getName());
    }
}
public void createNamespace() throws IOException {
    // 1. 创建NamespaceDescriptor
    NamespaceDescriptor ns1Descriptor = NamespaceDescriptor.create("ns1").build();
    ns1Descriptor.setConfiguration("name", "lixi");
    // 2. 添加到admin
    admin.createNamespace(ns1Descriptor);
}
public void listNamespaceTables() throws IOException {
    // 1. 查询指定的 namespace 中有多少表
    TableName[] tableNames = admin.listTableNamesByNamespace("hbase");
    // 2. 遍历
    for(TableName tableName:tableNames) {
        System.out.println(tableName.getNameAsString());
    }
}
public void alterNamespaceTables() throws IOException {
    // 1. 获取到 namespace 的数组
    NamespaceDescriptor ns1Descriptor = admin.getNamespaceDescriptor("ns1");
    // 2. 打印 namespace
    ns1Descriptor.setConfiguration("name", "lee");
    // 3. 提交修改
    admin.modifyNamespace(ns1Descriptor);
}
public void dropNamespace() throws IOException {
    admin.deleteNamespace("ns1");
}

Table 的 CRUD

public void createTable() throws IOException {
    // 1. 创建表的表述器对象
    HTableDescriptor tableDescriptor = new HTableDescriptor(TableName.valueOf("t1"));
    // 2. 创建列簇的描述器
    HColumnDescriptor familyColumn = new HColumnDescriptor("f1");
    // 3. 添加列簇
    tableDescriptor.addFamily(familyColumn);
    // 4. 提交表的描述器从而建表
    admin.createTable(tableDescriptor);
}
public void listTableColumnFamilies() throws IOException {
    // 1. 获取到表的描述器
    HTableDescriptor user_info = admin.getTableDescriptor(TableName.valueOf("user_info"));
    // 2. 获取到列簇信息
    HColumnDescriptor[] columnFamilies = user_info.getColumnFamilies();
    // 3. 遍历
    for(HColumnDescriptor columnFamily : columnFamilies) {
        System.out.println(columnFamily.getNameAsString());
    }
}
public void alterTable() throws IOException {
    // 1. 获取到表的描述器
    TableName tableName = TableName.valueOf("user_info");
    HTableDescriptor user_info = admin.getTableDescriptor(tableName);
    // 2. 获取到列簇信息
    HColumnDescriptor familyColumn1 = new HColumnDescriptor("base_info");
    user_info.addFamily(familyColumn1);
    //3. 提交修改
    admin.modifyTable(tableName, user_info);
}
public void deleteColumnFamily1() throws IOException {
    // 1. 获取到表的描述器
    TableName tableName = TableName.valueOf("user_info");
    HTableDescriptor user_info = admin.getTableDescriptor(tableName);
    // 2. 删除列簇
    user_info.removeFamily(Bytes.toBytes("lixi_info"));
    // 3. 提交修改
    admin.modifyTable(tableName, user_info);
}

public void deleteColumnFamily2() throws IOException {
    // 1. 获取到表的描述器
    TableName tableName = TableName.valueOf("user_info");
    // 2. 提交删除
    admin.deleteColumn(tableName, Bytes.toBytes("rock_info"));
}
public void deleteTable() throws IOException {
    // 1. 获取到表的描述器
    TableName tableName = TableName.valueOf("user_info");
    // 2. 处理表是否失效
    if(!admin.isTableDisabled(tableName)) {
    admin.disableTable(tableName);
    }
    // 3. 删除表
    admin.deleteTable(tableName);
}

Table 的 DML 操作

// 单条插入
public void put1() throws IOException {
    // 1. 创建Put对象
    Put put = new Put(Bytes.toBytes("001"));
    // 2. 添加列数据
    put.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("name"), Bytes.toBytes("lixi"));
    // 3. 插入一条记录
    table.put(put);
}

// 多条插入
public void put2() throws IOException {
    // 1. 创建 Put 对象
    Put put1 = new Put(Bytes.toBytes("002"));
    Put put2 = new Put(Bytes.toBytes("003"));
    // 2. 添加列数据
    put1.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("name"), Bytes.toBytes("lixi"));
    put2.addColumn(Bytes.toBytes("base_info"), Bytes.toBytes("name"), Bytes.toBytes("lixi"));

    List<Put> plist = new ArrayList<>();
    plist.add(put1);
    plist.add(put2);
    // 3. 插入一条记录
    table.put(plist);
}
public void get() throws IOException {
    // 1. 创建get对象
    Get get = new Get(Bytes.toBytes("001"));
    // 2. 查询数据,返回结果对象
    Result result = table.get(get);
    // 3. 查询指定列簇
    NavigableMap<byte[], byte[]> navigableMap = result.getFamilyMap(Bytes.toBytes("base_info"));
    // 4. 遍历
    Set<Map.Entry<byte[], byte[]>> entries = navigableMap.entrySet();
    for(Map.Entry<byte[], byte[]> entry : entries) {
        System.out.println(new String(entry.getKey()) +"--->" + new String(entry.getValue()) );
    }
}
public void get2() throws IOException {
    // 1. 创建get对象
    Get get = new Get(Bytes.toBytes("001"));
    // 2. 查询数据,返回结果对象
    Result result = table.get(get);
    // 3. 获取cell的扫描器
    CellScanner cellScanner = result.cellScanner();
    // 4. 遍历扫描器
    while(cellScanner.advance()) {
        // 5. 获取单词扫描的Cell
        Cell cell = cellScanner.current();
        System.out.println(new String(cell.getFamilyArray(),cell.getFamilyOffset(), cell.getFamilyLength())); // 列簇
        System.out.println(new String(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength())); // 列名
        System.out.println(new String(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength())); // 列值
    }
}
public void get3() throws IOException {
    // 1. 创建get对象
    Get get = new Get(Bytes.toBytes("001"));
    // 2. 查询数据,返回结果对象
    Result result = table.get(get);
    // 3. 获取cell的扫描器
    CellScanner cellScanner = result.cellScanner();
    // 4. 遍历扫描器
    while(cellScanner.advance()) {
        // 5. 获取单词扫描的Cell
        Cell cell = cellScanner.current();
        System.out.println(new String(CellUtil.cloneRow(cell)));
        System.out.println(new String(CellUtil.cloneFamily(cell))); // 列簇
        System.out.println(new String(CellUtil.cloneQualifier(cell))); // 列名
        System.out.println(new String(CellUtil.cloneValue(cell))); // 列值
    }
}

过滤器

Hbase提供了高级的查询方法,Filter。 Filter可以根据簇、列、版本等更多的条件来对数据进行过滤,基于Hbase本身提供的三维有序(主键有序、列有序、版本有序),这些Filter可以高效的完成查询过滤的任务。 带有 Filte r条件的 RPC 查询请求会把 Filter 分发到各个 RegionServer,是一个服务器端(Server-side)的过滤器,这样也可以降低网络传输的压力。

要完成一个过滤的操作,至少需要两个参数。一个是抽象的操作符,另外一个就是具体的比较器

抽象操作符(比较运算符)

LESS <
LESS_OR_EQUAL <=
EQUAL =
NOT_EQUAL <>
GREATER_OR_EQUAL >=
GREATER >
NO_OP 排除所有

比较器(指定比较机制)

BinaryComparator 按字节索引顺序比较指定字节数组,采用 Bytes.compareTo(byte[])
BinaryPrefixComparator 跟前面相同,只是比较左端的数据是否相同
NullComparator 判断给定的是否为空
BitComparator 按位比较
RegexStringComparator 提供一个正则的比较器,仅支持 EQUAL 和非 EQUAL
SubstringComparator 判断提供的子串是否出现在 value 中
 // 1. 创建单列值的过滤器
SingleColumnValueFilter nameFilter = new SingleColumnValueFilter(
                "info".getBytes(), //列簇
                "name".getBytes(), //列
                CompareOp.EQUAL, 
                new SubstringComparator("刘晨"));

// 注意,如果不设置为 true,则那些不包含指定 column 的行也会返回,不会被过滤掉
nameFilter.setFilterIfMissing(true);

// 2. scan 设置过滤器
scan.setFilter(nameFilter);

相当于 name="刘晨" and age <= 33

 // 1. 创建单列值的过滤器
SingleColumnValueFilter nameFilter = new SingleColumnValueFilter(
                Bytes.toBytes("info"), //列簇
                Bytes.toBytes("name"), //列
                CompareOp.EQUAL, 
                new SubstringComparator("刘晨"));

SingleColumnValueFilter ageFilter = new SingleColumnValueFilter(
                Bytes.toBytes("info"), //列簇
                Bytes.toBytes("age"), //列
                CompareOp.LESS_OR_EQUAL, 
                Bytes.toBytes(33));

// 2. 创建过滤器链       MUST_PASS_ALL = and        MUST_PASS_ONE = or
FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL);
filterList.addFilter(nameFilter);
filterList.addFilter(ageFilter);

// 3. scan 设置过滤器链
scan.setFilter(filterList);

RegexStringComparator 支持用于值比较的正则表达式

RegexStringComparator comp = new RegexStringComparator("my.");   // any value that starts with 'my'
SingleColumnValueFilter filter = new SingleColumnValueFilter(
   cf,
  column,
  CompareOperaor.EQUAL,
  comp
  );
scan.setFilter(filter);

SubstringComparator 可用于确定给定的子字符串是否存在于某个值中,比较是不区分大小写的。

SubstringComparator comp = new SubstringComparator("y val");   // looking for 'my value'
SingleColumnValueFilter filter = new SingleColumnValueFilter(
  cf,
  column,
  CompareOperaor.EQUAL,
  comp
  );
scan.setFilter(filter);

二进制前缀标签,判断是不是以某个数据开头

按字节索引顺序比较指定字节数组,相当于字符串比较

列簇的过滤器。过滤出包含列簇的信息

// 数据结构
private static String[] cfs = new String[]{"f1","f2"};
private static String[] data = new String[]{"row-1:f1:c1:v1", "row-2:f1:c2:v2", "row-3:f2:c3:v3", "row-4:f2:c4:v4"};

FamilyFilter familyFilter = new FamilyFilter(CompareFilter.CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("f1"))); 
// 过滤的结果是 [row-1, row-2]
scan.setFilter(filter1); 

列名的过滤器。

// 数据结构
private static String[] cfs = new String[]{"f"};
private static String[] data = new String[]{
            "row-1:f:name:Wang", "row-1:f:age:20",
            "row-2:f:name:Zhou", "row-2:f:age:10",
            "row-3:f:gender:男", "row-3:f:name:Li",
            "row-4:f:namana:xyz", "row-4:f:age:Zhao"
};

QualifierFilter qualifierFilter = new QualifierFilter(CompareFilter.CompareOp.EQUAL, new BinaryComparator(Bytes.toBytes("age")));

// 过滤的结果是   [row-1:f:age, row-2:f:age, row-4:f:age]
scan.setFilter(filter1); 

跟上面差不多,以什么列名开头。

多个列名前缀,比如,以 a 为开头,或者 以 b 为开头的。

RowFilter 基于行键进行过滤,在工作中涉及到需要通过HBase Rowkey进行数据过滤时可以考虑使用它。

基于列值进行过滤,在工作中涉及到需要通过HBase 列值进行数据过滤时可以考虑使用它。

布隆过滤器

首先要了解 HBase 的块索引机制。块索引是 HBase 固有的一个特性,因为 HBase 的底层数据是存储在 HFile 中的,而每个 HFile 中存储的是有序的<key, value>键值对,HFile 文件内部由连续的块组成,每个块中存储的第一行数据的行键组成了这个文件的块索引,这些块索引信息存储在文件尾部。当 HBase 打开一个 HFile 时,块索引信息会优先加载到内存;HBase首先在内存的块索引中进行二分查找,确定可能包含给定键的块,然后读取磁盘块找到实际想要的键。

但实际应用中,仅仅只有块索引满足不了需求,这是因为,块索引能帮助我们更快地在一个文件中找到想要的数据,但是我们可能依然需要扫描很多文件。而布隆过滤器就是为解决这个问题而生。因为布隆过滤器的作用是,用户可以立即判断一个文件是否包含特定的行键,从而帮我们过滤掉一些不需要扫描的文件

如下图所示,块索引显示每个文件中都可能包含对应的行键,而布隆过滤器能帮我们跳过一些明显不包含对应行键的文件。

スクリーンショット 2020-11-23 15 23 45

Hbase 寻址机制

寻址过程

client-->Zookeeper-->-ROOT-表-->.META.表-->RegionServer-->Region-->client
スクリーンショット 2020-11-25 19 25 18
  1. Client访问Zookeeper,查找-ROOT-表,获取.META.表信息。

  2. .META.表查找,获取存放目标数据的 Region 信息,从而找到对应的 RegionServer。

  3. 通过RegionServer获取需要查找的数据。

  4. Regionserver 的内存分为 MemStore 和 BlockCache 两部分,MemStore 主要用于写数据,BlockCache 主要用于读数据。 读请求先到 MemStore 中查数据,查不到就到 BlockCache 中查,再查不到就会到 StoreFile 上读,并把读的结果放入 BlockCache。

表包含 .META. 表所在的region列表,该表只会有一个Region。 Zookeeper中记录了-ROOT-表的location。

表包含所有的用户空间 region 列表,以及 RegionServer 的服务器地址。