Hbase

Hbase

HBase简介

1.1 HBase 定义

HBase 是一种分布式、可扩展、支持海量数据存储的 NoSQL 数据库。

HBase – Hadoop Database,是一个高可靠性、高性能、面向列、可伸缩、实时读写的分布式NoSQL数据库

利用Hadoop HDFS作为其文件存储系统,利用Hadoop MapReduce来处理HBase中的海量数据,利用Zookeeper作为其分布式协同服务

主要用来存储非结构化和半结构化的松散数据(列存 NoSQL 数据库)

逻辑上,HBase的数据模型同关系型数据库很类似,数据存储在一张表中,有行有列。但从HBase的底层物理存储结构(K-V)来看,HBase更像是一个multi-dimensional map

1.2 HBase 背景

HBase 的原型是 Google 的 BigTable 论文,受到了该论文思想的启发,目前作为 Hadoop的子项目来开发维护,用于支持结构化的数据存储。

2006 年 Google 发表 BigTable 白皮书
2006 年开始开发 HBase
2008 年北京成功开奥运会,程序员默默地将 HBase 弄成了 Hadoop 的子项目
2010 年 HBase 成为 Apache 顶级项目
现在很多公司二次开发出了很多发行版本
  HBase 是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,利用 HBASE 技术可在廉价 PC Server 上搭建起大规模结构化存储集群。HBase 的目标是存储并处理大型的数据,更具体来说是仅需使用普通的硬件配置,就能够处理由成千上万的行和列所组成的大型数据。

HBase 是 Google Bigtable 的开源实现,但是也有很多不同之处。比如:

Google Bigtable 利用 GFS 作为其文件存储系统,HBase 利用 Hadoop HDFS 作为其文件存储系统;
Google 运行 MapReduce 来处理 Bigtable 中的海量数据,HBase 同样利用 Hadoop MapReduce 来处理 HBase 中的海量数据;
Google Bigtable 利用 Chubby 作为协同服务,HBase 利用 Zookeeper 作为对应。

1.3 HBase 数据模型

逻辑上,HBase 的数据模型同关系型数据库很类似,数据存储在一张表中,有行有列。但从 HBase 的底层物理存储结构(K-V)来看,HBase 更像是一个 multi-dimensional map。

1.3.1 HBase 逻辑结构

在这里插入图片描述

1.3.2 HBase 物理存储结构

在这里插入图片描述

1.3.3 数据模型
  • NameSpace

命名空间,类似于关系型数据库的 DatabBase 概念,每个命名空间下有多个表。HBase 有两个自带的命名空间,分别是 hbase 和 default,hbase 中存放的是 HBase 内置的表,default 表是用户默认使用的命名空间。

  • Region

类似于关系型数据库的表概念。不同的是,HBase 定义表时只需要声明列族即可,不需要声明具体的列。这意味着,往 HBase 写入数据时,字段可以动态、按需指定。因此,和关系型数据库相比,HBase 能够轻松应对字段变更的场景。

  • Row

HBase 表中的每行数据都由一个 RowKey 和多个 Column(列)组成,数据是按照 RowKey 的字典顺序存储的,并且查询数据时只能根据 RowKey 进行检索,所以 RowKey 的设计十分重要。

  • Column

HBase 中的每个列都由 Column Family(列族)和 Column Qualifier(列限定符)进行限
定,例如 info:name,info:age。建表时,只需指明列族,而列限定符无需预先定义。

  • Time Stamp

用于标识数据的不同版本(version),每条数据写入时,如果不指定时间戳,系统会
自动为其加上该字段,其值为写入 HBase 的时间。

  • Cell

由 {rowkey, column Family:column Qualifier, time Stamp} 唯一确定的单元。cell 中的数
据是没有类型的,全部是字节码形式存贮。

1.4 HBase 基本架构

(不完整版)

  • RegionServer

Region Server 为 Region 的管理者,其实现类为 HRegionServer,主要作用如下:

① 对于数据的操作:get, put, delete;
② 对于 Region 的操作:splitRegion、compactRegion。

  • Master

Master 是所有 RegionServer 的管理者,其实现类为 HMaster,主要作用如下:

① 对于表的操作:create, delete, alter
② 对于 RegionServer的操作:分配 regions 到每个 RegionServer,监控每个 RegionServer 的状态,负载均衡和故障转移。

  • Zookeeper

HBase 通过 Zookeeper 来做 Master 的高可用、RegionServer 的监控、元数据的入口以及
集群配置的维护等工作。

  • HDFS

HDFS 为 HBase 提供最终的底层数据存储服务,同时为 HBase 提供高可用的支持。

安装

基于hadoop和zookeeper下

1.验证Hadoop平台是否正常
2.验证Zookeeper是否正常
3.解压并修改配置信息
4.同步节点
5.启动并验证

详细见安装目录下

HBase Shell 操作

基本操作

1.进入HBase客户端

hbase shell

2.查看帮助命令

help

3.查看当前数据库中有哪些表

list

表的操作

1.创建表

create 'student','info'

2.插入数据到表

put 'student','1001','info:sex','male'

put 'student','1001','info:age','18'

put 'student','1002','info:name','Janna'

put 'student','1002','info:sex','female'

put 'student','1002','info:age','20'

3.扫描查看表数据

scan 'student'

在这里插入图片描述

scan 'student',{STARTROW => '1001', STOPROW  => '1001'}

scan 'student',{STARTROW => '1001'}

4.查看表结构

describe 'student'

5.更新指定字段的数据

put 'student','1001','info:name','Nick'

put 'student','1001','info:age','100'

6.查看 “指定行” 或 “指定列族:列” 的数据

get 'student','1001'

get 'student','1001','info:name'

7.统计表数据行数

count 'student'

8.变更表信息

将 info 列族中的数据存放 3 个版本

alter 'student',{NAME=>'info',VERSIONS=>3}

get 'student','1001',{COLUMN=>'info:name',VERSIONS=>3}

9.删除数据

① 删除某 rowkey 的全部数据

deleteall 'student','1001'

② 删除某 rowkey 的某一列数据

delete 'student','1002','info:sex'

10.清空表数据

truncate 'student'

提示:清空表的操作顺序为先 disable,然后再 truncate。

11.删除表

① 首先需要先让该表为 disable 状态

disable 'student'

② 然后才能 drop 这个表

drop 'student'

命名空间的基本操作

1.查看命名空间

list_namespace

2.创建命名空间

create_namespace 'bigdata'

3.在新的命名空间中创建表

create 'bigdata:student','info'

4.删除命名空间

只能删除空的命名空间,如果不为空,需要先删除该命名空间下的所有表

drop_namespace 'bigdata'

HBASE SHELL

Group name: namespace

对数据库的操作

Commands: alter_namespace, create_namespace, describe_namespace, drop_namespace, list_namespace, list_namespace_tables
创建

create_namespace
create_namespace 'bigdata28'

查看

list_namespace 

NAMESPACE                                     
bigdata28 -- 创建的
default --默认命名空间
hbase  --用于存储命名空间以及表的元数据信息

查看详细信息

describe_namespace

describe_namespace 'bigdata28'

删除命名空间

drop_namespace
drop_namespace 'bigdata28'

查看命名空间下的表

list_namespace_tables

list_namespace_tables 'default'

Group name: ddl

对表进行操作

Commands: alter, alter_async, alter_status, clone_table_schema, create, descr
ibe, disable, disable_all, drop, drop_all, enable, enable_all, exists, get_table, is_disabled, is_enabled, list, list_regions, locate_region, show_filters
创建表

create 
# t 表示表名称
# f 表示列族

create 't1', 'f1', 'f2', 'f3'

create 'bigdata28:veh_pass','veh_info','pass_info'

查看表

list_namespace_tables 'bigdata28'

list

查看表的描述信息

describe "bigdata28:veh_pass"

# 每个列族都有对应一个描述信息 
#  VERSIONS 表示当前列族中存储的数据对应的最多的版本数
{NAME => 'pass_info', VERSIONS => '1', EVICT_BLOCKS_ON_CLOSE => 'false', NEW_VERSION_BEHAVIOR => 'false', KEEP_DELETED_CELLS => 'FALSE', CACHE_DATA_ON_WRITE => 'false', DATA_BLOCK_ENCODING => 'NONE', TTL => 'FOREVER', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', CACHE_INDEX_ON_WRITE => 'false', IN_MEMORY => 'false', CACHE_BLOOMS_ON_WRITE => 'false', PREFETCH_BLOCKS_ON_OPEN => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536'}                                                                                                                
{NAME => 'veh_info', VERSIONS => '1', EVICT_BLOCKS_ON_CLOSE => 'false', NEW_VERSION_BEHAVIOR => 'false', KEEP_DELETED_CELLS => 'FALSE', CACHE_DATA_ON_WRITE => 'false', DATA_BLOCK_ENCODING => 'NONE', TTL => 'FOREVER', MIN_VERSIONS => '0', REPLICATION_SCOPE => '0', BLOOMFILTER => 'ROW', CACHE_INDEX_ON_WRITE => 'false', IN_MEMORY => 'false', CACHE_BLOOMS_ON_WRITE => 'false', PREFETCH_BLOCKS_ON_OPEN => 'false', COMPRESSION => 'NONE', BLOCKCACHE => 'true', BLOCKSIZE => '65536'}

修改表信息

alter

alter 'bigdata28:veh_pass', {NAME => 'pass_info', IN_MEMORY => 'true'}
# 表示将该列族的数据放入内存中,以加快对该列族数据的访问速度。

# 表示将当前pass_info列族中的数据版本设置为最高可保留3个版本的数据 
alter 'bigdata28:veh_pass', {NAME => 'pass_info', VERSIONS => 3}

删除表

drop

drop 't1'
# 对于表删除时,需要对其进行关闭之后再进行删除
ERROR: Table t1 is enabled. Disable it first.

关闭或开启表

disable 't1'
enable 't1'

查看表的Region信息

list_regions 

list_regions "bigdata28:veh_pass"

Hbase中的数据是存储在HDFS中,那么如何进行存储的?
Hbase的数据存储目录在 /hbase/data/ 路径中
命名空间、表都是以目录的形式存在的
在表目录下,会存在有一个Region目录,Region是按照RowKey进行划分的(当数据量较少时,仅有一个) 在Region下对应有列族的目录,列族目录下存储具体的数据 ,当数据添加到HBASE后先放在内存当中,之后再保存到对于HDFS路径中

Group name: dml

对数据进行操作

Commands: append, count, delete, deleteall, get, get_counter, get_splits, inc
r, put, scan, truncate, truncate_preserve

添加数据

put 
# ns1 表示命名空间
# t1 表示表名称
# r1 表示RowKey信息 
# c1 表示列族及列信息 
# value 表示对应具体列下的单个数据 
put 'ns1:t1', 'r1', 'c1', 'value'

put 'bigdata28:veh_pass','pass1','veh_info:hphm','ASJ666'
put 'bigdata28:veh_pass','pass1','veh_info:cllx','K33'
put 'bigdata28:veh_pass','pass1','veh_info:clys','red'


put 'bigdata28:veh_pass','pass12','veh_info:hphm','ASJ888'
put 'bigdata28:veh_pass','pass12','veh_info:cllx','K13'
put 'bigdata28:veh_pass','pass12','veh_info:clys','w'


# 添加数据时指定时间戳
put 't1', 'r1', 'c1', 'value', ts1

put 'bigdata28:veh_pass','pass9','veh_info:hphm','ASJ000_5', 1709279201100
# 展示数据时,按照时间戳从大到小依次展示

put命令可以按照 Rowkey 列族 列 添加单个数据

查看数据

get

get 'ns1:t1', 'r1'
get 'bigdata28:veh_pass','pass1'

# 指定列进行查看数据
get 'bigdata28:veh_pass','pass9', {COLUMN => ['veh_info:cllx', 'veh_info:hphm']}

# Column family cllx does not exist 表示给定的列族不存在,列需要使用列族对其进行修饰

get 可以根据rowKey信息获取到一行数据

查看所有数据

scan 
scan 'bigdata28:veh_pass'

# 扫描某个列下所有数据
scan 'bigdata28:veh_pass',{COLUMNS => 'veh_info:hphm'}
# 获取多列数据
scan 'bigdata28:veh_pass',{COLUMNS => ['veh_info:hphm','veh_info:cllx']}

# 限制展示行数(RowKey)
scan 'bigdata28:veh_pass',{LIMIT => 3}
scan 'bigdata28:veh_pass',{COLUMNS => 'veh_info:hphm',LIMIT => 3}


# 指定范围过滤数据
# STARTROW指定开始的RowKey
# STARTROW是包含的 
scan 'bigdata28:veh_pass',{STARTROW => 'pass12'}
# STOPROW可以指定结束Rowkey,并且不包含
scan 'bigdata28:veh_pass',{STARTROW => 'pass12',STOPROW => 'pass8'}

# 案例:
# 需求:获取pass1开头的所有数据 
scan 'bigdata28:veh_pass',{STARTROW => 'pass1',STOPROW => 'pass8'}
# 当添加了pass7,当前的扫描的数据不符合规范
scan 'bigdata28:veh_pass',{STARTROW => 'pass1',STOPROW => 'pass1~'}
# 在设定STARTROW及STOPROW范围时,可以参考ASC码表中的最大值和最小值 

# 扫描所有数据
scan 'bigdata28:veh_pass', {RAW => true, VERSIONS => 4}

scan 可以扫描当前表中的所有数据

删除

delete 'ns1:t1', 'r1', 'c1', ts1

delete 'bigdata28:veh_pass','pass9','veh_info:hphm', 1709279201176

# 删除某个RowKey下的 一列
delete 'bigdata28:veh_pass','pass9','veh_info:hphm'

# 当删除数据时,实际上是对数据进行标记 type=Delete
# 在之后的某一时刻会对标记数据进行批量整理 

deleteall
deleteall 'bigdata28:veh_pass','pass9'

# 删除列族信息 可以使用表的Alter语法
alter 'bigdata28:veh_pass',{ METHOD => 'delete', NAME => 'pass_info' }

count

# 统计表的行数
count 'bigdata28:veh_pass'

truncate

# 清空表
# 立即生效
truncate 'bigdata28:veh_pass'

HBASE名词

Rowkey

唯一标识一行数据
可以通过RowKey获取一行数据
按照字典顺序排序的:字典序排序规则是按位置比较,比较大小是按照ASC码值进行比较
Row key只能存储64k的字节数据 一般情况下RowKey在 10-100byte 之间最为合适

put 'bigdata28:veh_pass','pass1','veh_info:hphm','ASJ777'
put 'bigdata28:veh_pass','pass1','veh_info:cllx','K31'
put 'bigdata28:veh_pass','pass1','veh_info:clys','w'

get 'bigdata28:veh_pass','pass1'


put 'bigdata28:veh_pass','pass8','veh_info:hphm','ASJ999'
put 'bigdata28:veh_pass','pass8','veh_info:cllx','K32'
put 'bigdata28:veh_pass','pass8','veh_info:clys','r'

put 'bigdata28:veh_pass','pass10','veh_info:hphm','ASJ000'
put 'bigdata28:veh_pass','pass10','veh_info:cllx','K30'
put 'bigdata28:veh_pass','pass10','veh_info:clys','w'

Column Family(列族)和qualifier(列)

HBase表中的每个列都归属于某个列族(簇),列族必须作为表模式(schema)定义的一部分预先给出。如 create ‘test’, ‘course’。
列名以列族作为前缀,每个“列族”都可以有多个列成员(column);如course:math, course:english, 新的列族成员(列)可以随后按需、动态加入。权限控制、存储以及调优都是在列族层面进行的;
HBase把同一列族里面的数据存储在同一目录下,由几个文件保存。(在HDFS的列族目录下,可以查看到)

Timestamp时间戳

在HBase每个cell存储单元对同一份数据有多个版本,根据唯一的时间戳来区分每个版本之间的差异,不同版本的数据按照时间倒序排序,最新的数据版本排在最前面。

alter 'bigdata28:veh_pass', {NAME => 'veh_info', VERSIONS => 3}
# 将veh_pass中的veh_info列族版本设置为3,其列族下的列,可以同时存储三个版本的时间戳数据

put 'bigdata28:veh_pass','pass9','veh_info:hphm','ASJ000_1'
put 'bigdata28:veh_pass','pass9','veh_info:cllx','K30_1'
put 'bigdata28:veh_pass','pass9','veh_info:clys','w_1'

put 'bigdata28:veh_pass','pass9','veh_info:hphm','ASJ000_2'
put 'bigdata28:veh_pass','pass9','veh_info:cllx','K30_2'
put 'bigdata28:veh_pass','pass9','veh_info:clys','w_2'

put 'bigdata28:veh_pass','pass9','veh_info:hphm','ASJ000_3'
put 'bigdata28:veh_pass','pass9','veh_info:cllx','K30_3'
put 'bigdata28:veh_pass','pass9','veh_info:clys','w_3'

get 'bigdata28:veh_pass','pass9' 
# get命令只能获取最新版本的数据

scan 'bigdata28:veh_pass', {RAW => true, VERSIONS => 3}
扫描全表时,可以获取多个版本的数据 


put 'bigdata28:veh_pass','pass9','veh_info:hphm','ASJ000_4'
put 'bigdata28:veh_pass','pass9','veh_info:cllx','K30_4'
put 'bigdata28:veh_pass','pass9','veh_info:clys','w_4'

scan 'bigdata28:veh_pass', {RAW => true, VERSIONS => 4}
# 对于HBASE中的数据操作,并不是一个实时的。对于不符合要求的数据,会通过定期检查,再将数据去除

Cell单元格

由行和列的坐标交叉决定。
单元格是有版本的。
单元格的内容是未解析的字节数组。
由 {row key, column( = +), version} 唯一确定的单元。
cell中的数据是没有类型的,全部是字节码形式存贮。(对于JavaAPI调用时,需要指定数据类型)

put 'bigdata28:veh_pass','pass9','veh_info:hpzl','大型汽车'
# 对于命令行,如果出现有中文,那么展示时,无法对其进行直接查看,英文可以由命令行转换


put 'bigdata28:veh_pass','pass9','pass_info:hpzl','大型汽车'
# 列是归属于列族的,不同列族之间的列名可以相同 
#  一般情况下相同类型的列会放到一个列族下,所以列名一般不重复

HBase架构分析

HMaster

1.为Region server分配region (当表中添加数据时,产生的Region具体是由HMaster将其分配给从节点HRegionServer)
2.负责Region server的负载均衡 (当表在使用过程中,一开始仅有一个Region,当数据量不断增大时,会对Region进行分裂成多个,分裂的Region是由HMaster进行指定到具体的从节点)
3.发现失效的Region server并重新分配其上的region(如果当Region server 宕机,此时需要将该RegionServer上的数据分配给其他节点)
4.管理用户对table的增删改操作(对表中元数据的操作,需要请求Master,但是数据的读写,并不是由HMaster管理)

RegionServer

1.Region server维护region,处理对这些region的IO请求(负责对HMaster分配的Region进行数据的检查更新操作,同时客户端的数据访问,会直接请求HRegionServer)
2.Region server负责切分在运行过程中变得过大的region(当表中的数据不断添加,数据是存储在Region中,当Region数据量过大时,会降低查询效率,于是需要将Region中的数据划分成多个Region进行分开存储)

Region

1.HBase自动把表水平划分成多个区域(region),每个region会保存一个表里面某段连续的数据(对于表中的RowKey是按照字典序排序,同时按照RowKey进行划分Region);每个表一开始只有一个region(但是建表时,也可以指定Region划分的规则生成多个Region),随着数据不断插入表,region不断增大,当增大到一个阀值的时候,region就会等分会两个新的region(裂变)

2.当table中的行不断增多,就会有越来越多的region。这样一张完整的表被保存在多个Regionserver 上。(通过该方式可以达到降低用户的单节点请求压力,当大量的用户请求被分配到少了的RegionServer上此时称为热点问题)

架构原理

读写流程

当数据被客户端提交到Hbase集群时,Hbase如何发现数据要存储的位置 => 数据存储位置是当前Hbase的元数据信息 =>Hbase元数据信息存储在什么位置?=> 通过查看namespace 发现有hbase命名空间,在该命名空间中对应有meta(表的元数据表,包括当前表的Region中保存的RowKey信息以及位置信息)namespace表示当前命名空间的元数据 => 当提交写入数据请求时,需要通过meta表知道表的Region存储位置 => 那么meta表客户端如何知道存储在什么位置?meta表也是一张Hbase表 => Zookeeper中/hbase路径下存在有meta-region-server, namespace节点,该节点中保存的数据为具体哪个节点中的RegionServer中 => 于是可以先去Meta表中的RegionServer中发现表的元数据 => 再去目标表中的RegionServer中添加数据

写操作

图片1.png

1)Client 先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server。 2)访问对应的 Region Server,获取 hbase:meta 表,根据写请求的 namespace:table/rowkey,查询出目标数据所在的 Region Server的Region信息,并将该 table 的 region 信息以及 meta 表的位置信息缓存在客户端的 meta cache,方便下次访问。
3)向Region Server发送写操作
4)将数据顺序写入(追加)到 WAL
5)将数据写入对应的 MemStore,数据会在 MemStore 进行排序
6)向客户端发送 ack响应信息
7)等达到 MemStore 的刷写时机后,将数据刷写到 HFile。

读操作

图片2.png

1)Client 先访问 zookeeper,获取 hbase:meta 表位于哪个 Region Server。2)访问对应的 Region Server,获取 hbase:meta 表,根据读请求的 namespace:table/rowkey,查询出目标数据位于哪个 Region Server 中的哪个 Region 中。并将该 table 的 region 信息以及 meta 表的位置信息缓存在客户端的 meta cache,方便下次访问。
3)与目标 Region Server 进行通讯
4) 当在MemStore中如果直接找到对应的数据,那么可以直接返回,如果找不到,再去BlockCache(读缓存,存放了HFile中的元数据信息)中进行查询,如果能查询到对应HFile,那么可以直接返回,如果查询到多个HFile那么对应会进行合并操作,将合并后的结果数据进行返回
5)将从文件中查询到的数据块元数据信息(Block,HFile 数据存储单元,默认大小为 64KB)缓存到Block Cache。
6)将合并后的最终结果返回给客户端。

API

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.shujia</groupId>
        <artifactId>bigdate28</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>hbase</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.apache.hbase/hbase-client -->
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>2.1.9</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
</project>

hbase连接

package com.shujia;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;

import java.io.IOException;

public class Code01HBaseConnection {
    public static void main(String[] args) throws IOException {
        /* 创建连接需要使用ConnectionFactory
           Java中的设计模式 -> 工厂模式 -> 可以使用静态方法获取其连接对象
         static Connection createConnection(Configuration conf)
        */

        Configuration configuration = new Configuration();
        /* 由Hbase的读写原理,知道客户端在连接Hbase做数据操作时,并不需要连接HMaster
              而需要连接Zookeeper,于是可以查看Hbase的配置文件,因为Hbase集群部署时
                填写了Zookeeper的配置信息,同时对于configuration.set中的Key和Value是
                   配置文件中的 name 和 Value 值
         */
//        configuration.set("hbase.zookeeper.quorum","localhost");
        configuration.set("hbase.zookeeper.quorum","master,node1,node2");
        Connection connection = ConnectionFactory.createConnection(configuration);

        // 获取具体的连接实现类? getClass => 获取类对象
        // org.apache.hadoop.hbase.client.ConnectionImplementation
        System.out.println("获取连接对象:"+connection.getClass().getName());

        /*
            当连接对象被获取,是否表明Hbase一定可用?
                不能表明,因为对于Hbase来说本身连接并没有添加到连接信息中
         */
        connection.close();


    }
}


创建了一个新的命名空间和表

package com.shujia;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;

public class Code02HbaseAdmin {
    Connection connection = null;
    Admin admin = null;
    @Before
    public void getCon() throws IOException {
        Configuration configuration = new Configuration();
        configuration.set("hbase.zookeeper.quorum","master:2181");
//        configuration.set("hbase.zookeeper.quorum","master:2181,node1:2181,node2:2181");
        connection = ConnectionFactory.createConnection(configuration);

        // 不添加配置对象,通过读取配置文件hbase-site.xml进行连接
//        connection = ConnectionFactory.createConnection();

        /*
            对于上述的 连接对象获取方式对比
                对于添加参数的方式,在代码中对配置信息写死会导致 当在公司测试环境中测试通过后,
                    将环境变更成开发环境后,需要对代码进行更改,再重新打包,比较耗时,同时工作流程不符合要求
                于是对配置文件中的信息进行修改,不需要对代码进行重新编译,更加方便规范
         */

        // 对于Admin对象可以对Hbase的元数据进行操作
        admin = connection.getAdmin();
    }

    @Test
    public void createNameSpace() throws IOException {
        // createNamespace(NamespaceDescriptor descriptor)
        NamespaceDescriptor.Builder builder = NamespaceDescriptor.create("api_2");
        NamespaceDescriptor namespaceDescriptor = builder.build();
        admin.createNamespace(namespaceDescriptor);
    }

    // 老API方式
    @Test
    public void createTableOld() throws IOException {
//        TableDescriptor desc
        /*
            Table should have at least one column family.
            创建表时必须要指定至少一个列族信息
         */
        TableName tableName = TableName.valueOf("api:t1");
        HTableDescriptor hTableDescriptor = new HTableDescriptor(tableName);
        // 添加列族信息
        //  HColumnDescriptor family
        hTableDescriptor.addFamily(new HColumnDescriptor("info"));
        hTableDescriptor.addFamily(new HColumnDescriptor("data"));
        admin.createTable(hTableDescriptor);
    }

    @Test
    public void createTableNew() throws IOException {
        /*
            在新API中使用TableDescriptorBuilder去构建TableDescriptor构建方式相对较为特殊
                需要先通过newBuilder传入TableName对象并通过build方法返回TableDescriptor的子类对象
                    该写法需要记住,以后在其他框架中也会有类似的构造形式 Builder.build方法构建
         */
        TableName tableName = TableName.valueOf("api:student");
        TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(tableName);
        // 在当前表描述类中发现有列族信息
        // ColumnFamilyDescriptorBuilder 以同样的方式Builder.build方法构建
        ColumnFamilyDescriptor scoreColumnFamily = ColumnFamilyDescriptorBuilder.newBuilder("score".getBytes()).build();
        ColumnFamilyDescriptor infoColumnFamily = ColumnFamilyDescriptorBuilder.of("info");
        // 通过set方式添加一个列族信息
        tableDescriptorBuilder.setColumnFamily(scoreColumnFamily);
        tableDescriptorBuilder.setColumnFamily(infoColumnFamily);
        TableDescriptor tableDescriptor = tableDescriptorBuilder.build();
        admin.createTable(tableDescriptor);
    }



    @After
    public void close() throws IOException {
        connection.close();
    }
}

检查一个表是否存在于HBase中和获取表的描述信息

package com.shujia;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;

public class Code03AdminShow {
    Connection connection = null;
    Admin admin = null;
    @Before
    public void getCon() throws IOException {
        Configuration configuration = new Configuration();
        configuration.set("hbase.zookeeper.quorum","master:2181");
//        configuration.set("hbase.zookeeper.quorum","master:2181,node1:2181,node2:2181");
        connection = ConnectionFactory.createConnection(configuration);

        // 不添加配置对象,通过读取配置文件hbase-site.xml进行连接
//        connection = ConnectionFactory.createConnection();

        /*
            对于上述的 连接对象获取方式对比
                对于添加参数的方式,在代码中对配置信息写死会导致 当在公司测试环境中测试通过后,
                    将环境变更成开发环境后,需要对代码进行更改,再重新打包,比较耗时,同时工作流程不符合要求
                于是对配置文件中的信息进行修改,不需要对代码进行重新编译,更加方便规范
         */

        // 对于Admin对象可以对Hbase的元数据进行操作
        admin = connection.getAdmin();
    }

    @Test
    public void exists() throws IOException {
        // Hbase 区分表的大小写
        TableName tableName = TableName.valueOf("api:Student");
        boolean tableExists = admin.tableExists(tableName);
        System.out.println(new String(tableName.getName())+"是否存在:"+tableExists);
    }

    @Test
    public void desc() throws IOException {
        TableDescriptor descriptor = admin.getDescriptor(TableName.valueOf("api:student"));
        // 获取表中所有列族的信息
        ColumnFamilyDescriptor[] columnFamilies = descriptor.getColumnFamilies();
        for (ColumnFamilyDescriptor columnFamily : columnFamilies) {
            String columnFamilyName = new String(columnFamily.getName());
            System.out.println("columnFamilyName:"+columnFamilyName);
        }

    }


    @After
    public void close() throws IOException {
        connection.close();
    }

}

表添加数据

package com.shujia;

import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class Code04Table {
    Connection connection = null;
    Admin admin = null;

    @Before
    public void getCon() throws IOException {
        connection = ConnectionFactory.createConnection();
        admin = connection.getAdmin();
    }

    @Test
    public void putOneData() throws IOException, ParseException {
        // 需要获取指定表的对象
        TableName tableName = TableName.valueOf("api:student");
        Table student = connection.getTable(tableName);

        /*
            由于Table中的put方法需要获取Put类对象,于是可以通过new 方式获取

            对于该数据 1500100001,施笑槐,22,女,文科六班 使用id信息作为RowKey
         */
//        Put put = new Put("1500100001".getBytes());
        // 当需要指定时间戳时
        // 2024-03-02 00:00:00
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = simpleDateFormat.parse("2024-03-02 00:00:00");
        long time = date.getTime();

        Put put = new Put("1500100001".getBytes(), time);
        // public Put addColumn(byte [] family, byte [] qualifier, byte [] value) {
        put.addColumn(
                "info".getBytes(),
                "name".getBytes(),
                "施笑槐".getBytes()
        );
        put.addColumn(
                "info".getBytes(),
                Bytes.toBytes("age"),
                Bytes.toBytes(22)
        );
        put.addColumn(
                "info".getBytes(),
                "gender".getBytes(),
                "女".getBytes()
        );
        put.addColumn(
                "info".getBytes(),
                "clazz".getBytes(),
                "文科六班".getBytes()
        );
        student.put(put);

    }

    @Test
    public void putOneData2() throws IOException, ParseException {
        Table table = connection.getTable(TableName.valueOf("api:student"));

        // 1500100002,吕金鹏,24,男,文科六班
        Put put = new Put(Bytes.toBytes("1500100002"));

        /*
        add 中需要添加一个Cell对象,而Cell表示行列交叉的一个Value数据
             并且由  RowKey + 列族+列 + 时间戳 决定
        KeyValue是Cell的自实现类 对应构造方法中存在以下参数:
        public KeyValue(byte[] row, byte[] family, byte[] qualifier, byte[] value) {
            this(row, family, qualifier, Long.MAX_VALUE, KeyValue.Type.Put, value);
        }
         */
        KeyValue name = new KeyValue(
                Bytes.toBytes("1500100002")
                , Bytes.toBytes("info")
                , Bytes.toBytes("name")
                , Bytes.toBytes("吕金鹏")
        );

        KeyValue age = new KeyValue(
                Bytes.toBytes("1500100002")
                , Bytes.toBytes("info")
                , Bytes.toBytes("age")
                , Bytes.toBytes(24)
        );

        KeyValue gender = new KeyValue(
                Bytes.toBytes("1500100002")
                , Bytes.toBytes("info")
                , Bytes.toBytes("gender")
                , Bytes.toBytes("男")
        );

        KeyValue clazz = new KeyValue(
                Bytes.toBytes("1500100002")
                , Bytes.toBytes("info")
                , Bytes.toBytes("clazz")
                , Bytes.toBytes("文科六班")
        );

        put.add(name);
        put.add(age);
        put.add(gender);
        put.add(clazz);
        table.put(put);
    }






    @After
    public void close() throws IOException {
        connection.close();
    }
}

读取文件内容填充表数据

package com.shujia;

import org.apache.hadoop.hbase.KeyValue;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.util.Bytes;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;


public class Code05PutMore {

    Connection connection = null;
    Admin admin = null;

    @Before
    public void getCon() throws IOException {
        connection = ConnectionFactory.createConnection();
        admin = connection.getAdmin();
    }


    @Test
    public void putMoreData() throws IOException {
        // 上传数据到Hbase中 需要将数据按条读取并插入
        BufferedReader bufferedReader = new BufferedReader(new FileReader("data/students.txt"));
        Table table = connection.getTable(TableName.valueOf("api:student"));

        String line = null;
        while ((line = bufferedReader.readLine()) != null) {
            String[] split = line.split(",");
            if (split.length == 5){
                // 如果切分的列为5 那么可以上传数据
                Put put = new Put(Bytes.toBytes(split[0]));

                KeyValue name = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("name")
                        , Bytes.toBytes(split[1])
                );

                KeyValue age = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("age")
                        , Bytes.toBytes(Integer.parseInt(split[2]))
                );

                KeyValue gender = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("gender")
                        , Bytes.toBytes(split[3])
                );

                KeyValue clazz = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("clazz")
                        , Bytes.toBytes(split[4])
                );

                put.add(name);
                put.add(age);
                put.add(gender);
                put.add(clazz);
                System.out.println("上传了一条数据:"+line);
                table.put(put);
            }
        }

        table.close();


    }


    // 对提交过程进行优化
    @Test
    public void putMoreData2() throws IOException {
        // 上传数据到Hbase中 需要将数据按条读取并插入
        BufferedReader bufferedReader = new BufferedReader(new FileReader("data/students.txt"));
        Table table = connection.getTable(TableName.valueOf("api:student_split"));

        String line = null;
        ArrayList<Put> putList = new ArrayList<>();

        while ((line = bufferedReader.readLine()) != null) {
            String[] split = line.split(",");
            if (split.length == 5){
                // 如果切分的列为5 那么可以上传数据
                Put put = new Put(Bytes.toBytes(split[0]));

                KeyValue name = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("name")
                        , Bytes.toBytes(split[1])
                );

                KeyValue age = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("age")
                        , Bytes.toBytes(Integer.parseInt(split[2]))
                );

                KeyValue gender = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("gender")
                        , Bytes.toBytes(split[3])
                );

                KeyValue clazz = new KeyValue(
                        Bytes.toBytes(split[0])
                        , Bytes.toBytes("info")
                        , Bytes.toBytes("clazz")
                        , Bytes.toBytes(split[4])
                );

                put.add(name);
                put.add(age);
                put.add(gender);
                put.add(clazz);
                putList.add(put);

                if (putList.size() >=100){
                    // 当List中数据达到100时,需要提交数据到HBASE中
                    table.put(putList);
                    System.out.println("100条数据提交成功...");
                    // 清空数据
                    putList.clear();
                }
            }
        }
        // 当数据读取完成后,需要再对不足100条数据的ArrayList进行提交
        table.put(putList);
        table.close();
    }

    @After
    public void close() throws IOException {
        connection.close();
    }
}

获取get方法表数据

package com.shujia;

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 org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Code06GetData {
    Connection connection = null;
    Admin admin = null;

    @Before
    public void getCon() throws IOException {
        connection = ConnectionFactory.createConnection();
        admin = connection.getAdmin();
    }

    @Test
    public void getData() throws IOException {
        Table table = connection.getTable(TableName.valueOf("api:student"));
        Get get = new Get(Bytes.toBytes("1500100002"));
        // Result 中保存了查询数据的结果 和 JDBC中的模式很像
        //  Result按照指针的方式获取数据,一开始指针位于数据集之前  需要使用advance将指针向前移动一位
        Result result = table.get(get);
        System.out.println(result.size()); // 4
//        while (result.advance()) {
//            Cell cell = result.current();
//            System.out.println(cell); // null
//            String familyName = Bytes.toString(cell.getFamilyArray());
//            String columnName = Bytes.toString(cell.getQualifierArray());
//            String value = Bytes.toString(cell.getValueArray());
//            System.out.println(familyName + ":" + columnName + "\t" + value);
//        }

//        while (result.advance()){
//            Cell cell = result.current();
//            String family = Bytes.toString(CellUtil.cloneFamily(cell));
//            String column = Bytes.toString(CellUtil.cloneQualifier(cell));
//            // 对于不同列的类型转换不一样,需要分开进行处理
//            if (column.equals("age")){
//                int value = Bytes.toInt(CellUtil.cloneValue(cell));
//                System.out.println(family+":"+column+"\t"+value);
//            }else {
//                String value = Bytes.toString(CellUtil.cloneValue(cell));
//                System.out.println(family+":"+column+"\t"+value);
//            }
//        }

        List<Cell> cells = result.listCells();
        for (Cell cell : cells) {
            String family = Bytes.toString(CellUtil.cloneFamily(cell));
            String column = Bytes.toString(CellUtil.cloneQualifier(cell));
            // 对于不同列的类型转换不一样,需要分开进行处理
            if (column.equals("age")){
                int value = Bytes.toInt(CellUtil.cloneValue(cell));
                System.out.println(family+":"+column+"\t"+value);
            }else {
                String value = Bytes.toString(CellUtil.cloneValue(cell));
                System.out.println(family+":"+column+"\t"+value);
            }
        }
    }

    @Test
    public void getMoreData() throws IOException {
        Table table = connection.getTable(TableName.valueOf("api:student"));
        ArrayList<Get> getList = new ArrayList<>();
        // Result 中保存了查询数据的结果 和 JDBC中的模式很像
        //  Result按照指针的方式获取数据,一开始指针位于数据集之前  需要使用advance将指针向前移动一位
        getList.add(new Get(Bytes.toBytes("1500100002")));
        getList.add(new Get(Bytes.toBytes("1500100003")));
        getList.add(new Get(Bytes.toBytes("1500100004")));

        for (Result result : table.get(getList)) {
            List<Cell> cells = result.listCells();
            for (Cell cell : cells) {
                String rowKey = Bytes.toString(CellUtil.cloneRow(cell));
                String family = Bytes.toString(CellUtil.cloneFamily(cell));
                String column = Bytes.toString(CellUtil.cloneQualifier(cell));
                // 对于不同列的类型转换不一样,需要分开进行处理
                if (column.equals("age")){
                    int value = Bytes.toInt(CellUtil.cloneValue(cell));
                    System.out.println(rowKey+"\t"+family+":"+column+"\t"+value);
                }else {
                    String value = Bytes.toString(CellUtil.cloneValue(cell));
                    System.out.println(rowKey+"\t"+family+":"+column+"\t"+value);
                }
            }
        }
    }


    @After
    public void close() throws IOException {
        connection.close();
    }
}

使用scan方法获取数据

package com.shujia;

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 org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Iterator;

public class Code07ScanTable {
    Connection connection = null;
    Admin admin = null;

    @Before
    public void getCon() throws IOException {
        connection = ConnectionFactory.createConnection();
        admin = connection.getAdmin();
    }

    @Test
    public void scanTable() throws IOException {
        Table table = connection.getTable(TableName.valueOf("api:student"));

        // 创建一个无参的Scan 对表中所有的数据进行遍历操作
        ResultScanner scanner = table.getScanner(new Scan());
        Result result = null;
        while ((result = scanner.next())!=null){
            for (Cell cell : result.listCells()) {
                String rowKey = Bytes.toString(CellUtil.cloneRow(cell));
                String family = Bytes.toString(CellUtil.cloneFamily(cell));
                String column = Bytes.toString(CellUtil.cloneQualifier(cell));
                // 对于不同列的类型转换不一样,需要分开进行处理
                if (column.equals("age")) {
                    int value = Bytes.toInt(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                } else {
                    String value = Bytes.toString(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                }
            }
        }
    }

    @Test
    public void scanTable2() throws IOException {
        Table table = connection.getTable(TableName.valueOf("api:student"));

        // 创建一个无参的Scan 对表中所有的数据进行遍历操作
        ResultScanner scanner = table.getScanner(new Scan());
        for (Result result : scanner) {
            for (Cell cell : result.listCells()) {
                String rowKey = Bytes.toString(CellUtil.cloneRow(cell));
                String family = Bytes.toString(CellUtil.cloneFamily(cell));
                String column = Bytes.toString(CellUtil.cloneQualifier(cell));
                // 对于不同列的类型转换不一样,需要分开进行处理
                if (column.equals("age")) {
                    int value = Bytes.toInt(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                } else {
                    String value = Bytes.toString(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                }
            }
        }
    }

    @Test
    public void scanTable3() throws IOException {
        Table table = connection.getTable(TableName.valueOf("api:student"));

        // 创建一个无参的Scan 对表中所有的数据进行遍历操作
        ResultScanner scanner = table.getScanner(new Scan());
        Iterator<Result> iterator = scanner.iterator();
        while (iterator.hasNext()) {
            Result result = iterator.next();
            for (Cell cell : result.listCells()) {
                String rowKey = Bytes.toString(CellUtil.cloneRow(cell));
                String family = Bytes.toString(CellUtil.cloneFamily(cell));
                String column = Bytes.toString(CellUtil.cloneQualifier(cell));
                // 对于不同列的类型转换不一样,需要分开进行处理
                if (column.equals("age")) {
                    int value = Bytes.toInt(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                } else {
                    String value = Bytes.toString(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                }
            }
        }
    }


    @After
    public void close() throws IOException {
        connection.close();
    }
}

范围获取数据

package com.shujia;

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 org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;

public class Code08ScanTableRange {
    Connection connection = null;
    Admin admin = null;

    @Before
    public void getCon() throws IOException {
        connection = ConnectionFactory.createConnection();
        admin = connection.getAdmin();
    }

    @Test
    public void scanTable() throws IOException {
        Table table = connection.getTable(TableName.valueOf("api:student"));

        Scan scan = new Scan(
                "15001002".getBytes(),
                "15001003~".getBytes()
        );

        ResultScanner scanner = table.getScanner(scan);
        Result result = null;
        while ((result = scanner.next())!=null){
            for (Cell cell : result.listCells()) {
                String rowKey = Bytes.toString(CellUtil.cloneRow(cell));
                String family = Bytes.toString(CellUtil.cloneFamily(cell));
                String column = Bytes.toString(CellUtil.cloneQualifier(cell));
                // 对于不同列的类型转换不一样,需要分开进行处理
                if (column.equals("age")) {
                    int value = Bytes.toInt(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                } else {
                    String value = Bytes.toString(CellUtil.cloneValue(cell));
                    System.out.println(rowKey + "\t" + family + ":" + column + "\t" + value);
                }
            }
        }
    }




    @After
    public void close() throws IOException {
        connection.close();
    }
}

过滤器使用[HBase 系列(七)——HBase过滤器详解_介绍一下hbase过滤器-CSDN博客]

使用MapReduce计算

数据准备

启动hdfs和hbase

start-dfs.sh

start-hbase.sh

进入hbase shell命令行

hbase shell

创建输入表word

创建输入表word
create 'word','content'
插入数据
put 'word','1001','content:info','when all else is lost the future still remains'
put 'word','1002','content:info','sow nothing reap nothing'
put 'word','1003','content:info','keep on going never give up'
put 'word','1004','content:info','the wealth of the mind is the only wealth'

创建输出表result

create 'result','content'

编码

新建maven工程
添加依赖

pom.xml添加如下依赖:

        <dependency>
         	 <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
            <version>1.7.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-server</artifactId>
            <version>1.7.1</version>
        </dependency>
编写Mapper
package org.example.mr;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.TableMapper;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import java.io.IOException;
//                                           Mapper输出k,v类型

public class WordCountMapper extends TableMapper<Text, IntWritable> {
    @Override
    protected void map(ImmutableBytesWritable key, Result value, Context context) throws IOException, InterruptedException {
        // getValue参数列族,列
        String data = Bytes.toString(value.getValue(Bytes.toBytes("content"), Bytes.toBytes("info")));
        String[] words = data.split(" ");
        for (String w : words) {
           context.write(new Text(w), new IntWritable(1));
        }
    }
}
编写Reducer
package org.example.mr;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.io.ImmutableBytesWritable;
import org.apache.hadoop.hbase.mapreduce.TableReducer;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import java.io.IOException;

//                                          数据类型 KEYIN, VALUEIN, KEYOUT
//                                                    k3,  v3,      rowkey
public class WordCountReducer extends TableReducer<Text, IntWritable, ImmutableBytesWritable> {
    @Override
    protected void reduce(Text k3, Iterable<IntWritable> v3, Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable i : v3) {
            sum = sum + i.get();
        }
        //输出:表中的一条记录 Put对象
        //使用单词作为行键
        Put put = new Put(Bytes.toBytes(k3.toString()));
        // 列族, 列限定符, 值
        put.addColumn(Bytes.toBytes("content"), Bytes.toBytes("count"), Bytes.toBytes(String.valueOf(sum)));
        //写入HBase
        context.write(new ImmutableBytesWritable(Bytes.toBytes(k3.toString())), put);
    }
}
编写Main
package org.example.mr;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.mapreduce.TableMapReduceUtil;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import java.io.IOException;

public class WordCountMain {
    public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
        // 获取配置,并设置hbase的zk地址
        Configuration conf = new Configuration();
        conf.set("hbase.zookeeper.quorum","192.168.193.140");
        // 创建job,并设置主类
        Job job = Job.getInstance(conf);
        job.setJarByClass(WordCountMain.class);
        // scan查询实例
        Scan scan = new Scan();
        // 添加查询的列,参数为列族、列
	 scan.addColumn(Bytes.toBytes("content"),Bytes.toBytes("info"));// 参数:列族,列
        // 设置job的读取输入的表,Mapper类,输出k,v类型
        TableMapReduceUtil.initTableMapperJob("word", scan, WordCountMapper.class,
                Text.class, IntWritable.class, job);
        // 设置job的Reducer类,及Reduce输出到的表
        TableMapReduceUtil.initTableReducerJob("result", WordCountReducer.class, job);
        // 执行job
        job.waitForCompletion(true);
    }
}

测试

IDEA运行main方法,报错如下:

img

解决方法为

pom.xml添加如下jackson依赖

        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-core-asl</artifactId>
            <version>1.9.13</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-mapper-asl</artifactId>
            <version>1.9.13</version>
        </dependency>

IDEA再次运行main方法,成功

查看结果

img

原理及优化

查询快的原因

A:如果快速查询(从磁盘读数据),hbase是根据rowkey查询的,只要能快速的定位rowkey, 就能实现快速的查询,主要是以下因素:
1、hbase是可划分成多个region,你可以简单的理解为关系型数据库的多个分区。
2、键是排好序了的
3、按列存储的

首先,能快速找到行所在的region(分区),假设表有10亿条记录,占空间1TB, 分列成了500个region, 1个region占2个G. 最多读取2G的记录,就能找到对应记录;

其次,是按列存储的,其实是列族,假设分为3个列族,每个列族就是666M, 如果要查询的东西在其中1个列族上,1个列族包含1个或者多个HStoreFile,假设一个HStoreFile是128M, 该列族包含5个HStoreFile在磁盘上. 剩下的在内存中。

再次,是排好序了的,你要的记录有可能在最前面,也有可能在最后面,假设在中间,我们只需遍历2.5个HStoreFile共300M

最后,每个HStoreFile(HFile的封装),是以键值对(key-value)方式存储,只要遍历一个个数据块中的key的位置,并判断符合条件可以了。 一般key是有限的长度,假设跟value是1:19(忽略HFile上其它块),最终只需要15M就可获取的对应的记录,按照磁盘的访问100M/S,只需0.15秒。 加上块缓存机制(LRU原则),会取得更高的效率。

B:实时查询
实时查询,可以认为是从内存中查询,一般响应时间在1秒内。HBase的机制是数据先写入到内存中,当数据量达到一定的量(如128M),再写入磁盘中, 在内存中,是不进行数据的更新或合并操作的,只增加数据,这使得用户的写操作只要进入内存中就可以立即返回,保证了HBase I/O的高性能。

实时查询,即反应根据当前时间的数据,可以认为这些数据始终是在内存的,保证了数据的实时响应。

布隆过滤器

对于Hbase可以由Rowkey进行划分Region,再由Region中的列族划分store,Store中有MemStore对数据进行缓存,对刷写的数据可以保存在HDFS中的HFile文件中,每次MemStore刷写都会生成一个HFile文件,如果要过滤数据 找到对应数据存储在对应哪个HFile中即可。于是对于布隆过滤器的使用,可以将HFile看成是一个集合,当数据添加到HFile中,可以由布隆过滤器维护一个二进制的向量,通过Hash函数对数据进行映射。当有数据要验证时,直接对RowKey数据进行hash计算,找到对应位置是否为1即可。过滤掉大部分的HFile,减少需要扫描的Block。误判对于查询结果影响并不大。

RowKey设计-三原则

Rowkey长度原则

rowkey是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般为10-100bytes原因如下:一、数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光rowkey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;二、MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。

Rowkey散列原则

Rowkey散列原则
如果rowkey按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将rowkey的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer,以提高负载均衡的几率。如果没有散列字段,首字段直接是时间信息,所有的数据都会集中在一个RegionServer上,这样在数据检索的时候负载会集中在个别的RegionServer上,造成热点问题,会降低查询效率。

如果Rowkey现定义以时间戳作为 Rowkey+随机值,那么Rowkey是有序的,对应会将连续的Rowkey发送到同一个RegionServer中,在存储时,如果数据量较大,那么大部分的写入请求会连续的写入到一个RegionServer中,没有形成分布式的效果。于是形成写热点问题。同时读取数据时,时间戳最近的数据,其访问频率相对较高,所以所有的读请求也会发送到对应少了的RegionServer中,也会形成读热点问题

rowkey唯一原则

必须在设计上保证其唯一性
经常读取的数据存储到一块,将最近可能会被访问的数据放到一块 => 当查询数据时,是按照Rowkey的区间对数据进行获取,相连续的数据会在一个HFile中存储,将遍历的数据一次性返回即可,而不需要再从多个Region中加载多个HFile文件进行返回。

RowKey热点问题

什么是热点问题?当大量的 client 访问 hbase 集群的一个或少数几个节点,造成少数 region server 的读/写请求过多、负载过大,而其他 region server 负载却很小,就造成了“热点”现象。

解决方案:

①加盐:在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。分配的前缀种类数量应该和你想使用数据分散到不同的region的数量一致。
优点:对数据进行均匀的分散到多个Region中,有效避免热点问题
缺点:要按照RowKey查询对应的Value数据 rand_id 时,会造成数据不确定存放在哪个位置,不能直接根据一个RowKey获取对应Value值

②哈希:哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据
id:1500001 =hash> 5a91734fe2f68e2cb93cd4c3d46776a7
通过生成的密文作为Rowkey可以有效的将数据进行分散开
优点:使负载分散到整个集群,但是读却是可以预测的
缺点:使用Scan方式扫描数据时,不能以明文的方式查看到对应ID的内容,同时也不能使用连续的方式扫描ID数据

③反转:使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性

20240304154101 直接使用时间戳会导致热点问题,于是使用反转的方式将变化快的数据放在前面,不变的放在后面
=> 10145104034202 当秒数发生变化时,对应连续时间数据数据会分散到多个Region中进行存储
优点:有效解决热点问题,同时数据内容可以有效展示,同时对于get方法获取数据时,也可以直接获取到其Value值
缺点:丢失了数据的连续性

④预分区:可以通过指定SPLITS_FILE的值指定分区文件,如果分区信息比较少,也可以直接用SPLITS分区。create ‘split_table_test’, ‘cf’, {SPLITS_FILE => ‘region_split_info.txt’}

思考:
当数据一开始保存到表中时,表中只会存在一个Region,如果插入的数据量较大,所有的写请求都会存入一个RegionServer中的1个Region中,其写入压力非常大,会影响其他表在同RegionServer中的Region的访问
如果一开始创建表时,就给定初始的Region,那么对应数据按照Region中的Rowkey范围进行划分, 那么就不存在有写入压力了
create ‘api:student_split’, ‘info’, {SPLITS_FILE => ‘/root/region_split_info.txt’}
region_split_info.txt:该文件中保存了每个Region的RowKey存储范围
1500100100!
1500100200!
1500100300!
1500100400!
1500100500!
1500100600!
1500100700!
1500100800!
1500100900!

在Hbase中如何判断是否存在有热点问题?

1.打开Hbase页面16010端口
2.找到对应表的描述页面
3.在页面中对每个Region的请求数量进行分析,当某个Region的请求过多时,说明存在有热点问题。需要对当前表进行RowKey重构

BulkLoading及整合HIVE[Hive数据导入hbase使用BulkLoad方式(spark和mapperReduce两种实现)_import org.apache.hadoop.hbase.mapred.tableoutputf-CSDN博客]

5. HBase 优化

5.1 高可用

在 HBase 中 HMaster 负责监控 HRegionServer 的生命周期,均衡 RegionServer 的负载,如果 HMaster 挂掉了,那么整个 HBase 集群将陷入不健康的状态,并且此时的工作状态并不会维持太久。所以 HBase 支持对 HMaster 的高可用配置。

  1. 关闭 HBase 集群
stop-hbase.sh
1
  1. 在 conf 目录下创建 backup-masters 文件,配置高可用 HMaster 节点
touch backup-masters
1
echo slave1 > backup-masters
1
  1. 同步配置
xsync backup-masters
1
  1. 启动 HBase
start-hbase.sh
1

5.2 预分区

每一个 region 维护着 StartRow 与 EndRow,如果加入的数据符合某个 Region 维护的 RowKey 范围,则该数据交给这个 Region 维护。那么依照这个原则,我们可以将数据所要投放的分区提前大致的规划好,以提高 HBase 性能。

  1. 手动设定预分区
create 'staff1','info','partition1',SPLITS => ['1000','2000','3000','4000']
1
  1. 生成 16 进制序列预分区
create 'staff2','info','partition2',{NUMREGIONS => 15, SPLITALGO => 'HexStringSplit'}
1
  1. 按照文件中设置的规则预分区

    创建 splits.txt 文件内容如下:

aaaa
bbbb
cccc
dddd

然后执行:

create 'staff3','partition3',SPLITS_FILE => 'splits.txt'

5.3 RowKey 设计

一条数据的唯一标识就是 RowKey,那么这条数据存储于哪个分区,取决于 RowKey 处于哪个一个预分区的区间内,设计 RowKey 的主要目的 ,就是让数据均匀的分布于所有的 region 中,在一定程度上防止数据倾斜。(散列性、唯一性、长度原则)

  1. 生成随机数、hash、散列值
  2. 字符串反转
20170524000001 转成 10000042507102
20170524000002 转成 20000042507102
12
  1. 字符串拼接
20170524000001_a12e
20170524000001_93i7
12

5.4 内存优化

HBase 操作过程中需要大量的内存开销,毕竟 Table 是可以缓存在内存中的,一般会分配整个可用内存的 70%给 HBase 的 Java 堆。但是不建议分配非常大的堆内存,因为 GC 过程持续太久会导致 RegionServer 处于长期不可用状态,一般 16~48G 内存就可以了,如果因为框架占用内存过高导致系统内存不足,框架一样会被系统服务拖死。

5.5 基础优化

  1. 允许在 HDFS 的文件中追加内容

    hdfs-site.xml、hbase-site.xml
    属性: dfs.support.append
    解释: 开启 HDFS 追加同步,可以优秀的配合 HBase 的数据同步和持久化。默认值为 true。

  2. 优化 DataNode 允许的最大文件打开数

    hdfs-site.xml
    属性: dfs.datanode.max.transfer.threads
    解释: HBase 一般都会同一时间操作大量的文件,根据集群的数量和规模以及数据动作,设置为 4096 或者更高。默认值:4096

  3. 优化延迟高的数据操作的等待时间

    hdfs-site.xml
    属性: dfs.image.transfer.timeout
    解释: 如果对于某一次数据操作来讲,延迟非常高,socket 需要等待更长的时间,建议把
    该值设置为更大的值(默认 60000 毫秒),以确保 socket 不会被 timeout 掉。

  4. 优化数据的写入效率

    mapred-site.xml
    属性: mapreduce.map.output.compress、mapreduce.map.output.compress.codec
    解释: 开启这两个数据可以大大提高文件的写入效率,减少写入时间。第一个属性值修改为 true,第二个属性值修改为:org.apache.hadoop.io.compress.GzipCodec 或者其他压缩方式。

  5. 设置 RPC 监听数量

    hbase-site.xml
    属性: Hbase.regionserver.handler.count
    解释: 默认值为 30,用于指定 RPC 监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。

  6. 优化 HStore 文件大小

    hbase-site.xml
    属性: hbase.hregion.max.filesize
    解释: 默认值 10737418240(10GB),如果需要运行 HBase 的 MR 任务,可以减小此值,因为一个 region 对应一个 map 任务,如果单个 region 过大,会导致 map 任务执行时间过长。该值的意思就是,如果 HFile 的大小达到这个数值,则这个 region 会被切分为两个 Hfile。

  7. 优化 HBase 客户端缓存

    hbase-site.xml
    属性: hbase.client.write.buffer
    解释: 用于指定 Hbase 客户端缓存,增大该值可以减少 RPC 调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少 RPC 次数的目的。

  8. 指定 scan.next 扫描 HBase 所获取的行数

    hbase-site.xml
    属性: hbase.client.scanner.caching
    解释: 用于指定 scan.next 方法获取的默认行数,值越大,消耗内存越大。

  9. flush、compact、split 机制

    当 MemStore 达到阈值,将 Memstore 中的数据 Flush 进 Storefile;
    compact 机制则是把 flush出来的小文件合并成大的 Storefile 文件;
    split 则是当 Region 达到阈值,会把过大的 Region一分为二。

    涉及属性:

hbase.hregion.memstore.flush.size:134217728
1

即:这个参数的作用是当单个 HRegion 内所有的 Memstore 大小总和超过指定值时,flush 该 HRegion 的所有 memstore。RegionServer 的 flush 是通过将请求添加一个队列,模拟生产消费模型来异步处理的。那这里就有一个问题,当队列来不及消费,产生大量积压请求时,可能会导致内存陡增,最坏的情况是触发 OOM。

hbase.regionserver.global.memstore.upperLimit:0.4
hbase.regionserver.global.memstore.lowerLimit:0.38
12

fs.support.append
解释: 开启 HDFS 追加同步,可以优秀的配合 HBase 的数据同步和持久化。默认值为 true。

  1. 优化 DataNode 允许的最大文件打开数

    hdfs-site.xml
    属性: dfs.datanode.max.transfer.threads
    解释: HBase 一般都会同一时间操作大量的文件,根据集群的数量和规模以及数据动作,设置为 4096 或者更高。默认值:4096

  2. 优化延迟高的数据操作的等待时间

    hdfs-site.xml
    属性: dfs.image.transfer.timeout
    解释: 如果对于某一次数据操作来讲,延迟非常高,socket 需要等待更长的时间,建议把
    该值设置为更大的值(默认 60000 毫秒),以确保 socket 不会被 timeout 掉。

  3. 优化数据的写入效率

    mapred-site.xml
    属性: mapreduce.map.output.compress、mapreduce.map.output.compress.codec
    解释: 开启这两个数据可以大大提高文件的写入效率,减少写入时间。第一个属性值修改为 true,第二个属性值修改为:org.apache.hadoop.io.compress.GzipCodec 或者其他压缩方式。

  4. 设置 RPC 监听数量

    hbase-site.xml
    属性: Hbase.regionserver.handler.count
    解释: 默认值为 30,用于指定 RPC 监听的数量,可以根据客户端的请求数进行调整,读写请求较多时,增加此值。

  5. 优化 HStore 文件大小

    hbase-site.xml
    属性: hbase.hregion.max.filesize
    解释: 默认值 10737418240(10GB),如果需要运行 HBase 的 MR 任务,可以减小此值,因为一个 region 对应一个 map 任务,如果单个 region 过大,会导致 map 任务执行时间过长。该值的意思就是,如果 HFile 的大小达到这个数值,则这个 region 会被切分为两个 Hfile。

  6. 优化 HBase 客户端缓存

    hbase-site.xml
    属性: hbase.client.write.buffer
    解释: 用于指定 Hbase 客户端缓存,增大该值可以减少 RPC 调用次数,但是会消耗更多内存,反之则反之。一般我们需要设定一定的缓存大小,以达到减少 RPC 次数的目的。

  7. 指定 scan.next 扫描 HBase 所获取的行数

    hbase-site.xml
    属性: hbase.client.scanner.caching
    解释: 用于指定 scan.next 方法获取的默认行数,值越大,消耗内存越大。

  8. flush、compact、split 机制

    当 MemStore 达到阈值,将 Memstore 中的数据 Flush 进 Storefile;
    compact 机制则是把 flush出来的小文件合并成大的 Storefile 文件;
    split 则是当 Region 达到阈值,会把过大的 Region一分为二。

    涉及属性:

hbase.hregion.memstore.flush.size:134217728
1

即:这个参数的作用是当单个 HRegion 内所有的 Memstore 大小总和超过指定值时,flush 该 HRegion 的所有 memstore。RegionServer 的 flush 是通过将请求添加一个队列,模拟生产消费模型来异步处理的。那这里就有一个问题,当队列来不及消费,产生大量积压请求时,可能会导致内存陡增,最坏的情况是触发 OOM。

hbase.regionserver.global.memstore.upperLimit:0.4
hbase.regionserver.global.memstore.lowerLimit:0.38
12

即:当 MemStore 使用内存总量达到 hbase.regionserver.global.memstore.upperLimit 指定值时,将会有多个 MemStores flush 到文件中,MemStore flush 顺序是按照大小降序执行的,直到刷新到 MemStore 使用内存略小于 lowerLimit。

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值