1、了解HBase
官方文档:Apache HBase – Apache HBase™ Home
什么是Hbase?
HBase 是一个高可靠性、高性能、面向列、可伸缩的分布式存储系统,用于存储海量的结构化或者半结构化,非结构化的数据(底层是字节数组做存储的)
- HBase 依赖于 HDFS 做底层的数据存储
- HBase 依赖于 MapReduce 做数据计算
- HBase 依赖于 ZooKeeper 做服务协调
1、HBase概述
HBase是Hadoop的生态系统之一,是建立在Hadoop文件系统(HDFS)之上的分布式、面向列的数据库,通过利用Hadoop的文件系统提供容错能力。如果需要进行实时读写或者随机访问大规模的数据集的时候,会考虑使用HBase。
HBase作为Google Bigtable的开源实现,Google Bigtable利用GFS作为其文件存储系统类似,则HBase利用Hadoop HDFS作为其文件存储系统;Google通过运行MapReduce来处理Bigtable中的海量数据,同样,HBase利用Hadoop MapReduce来处理HBase中的海量数据;Google Bigtable利用Chubby作为协同服务,HBase利用Zookeeper作为元数据的元数据存储和容灾。在2010年5月,成为apache顶级项目。
2、HBase处理数据
虽然Hadoop是一个高容错、高延时的分布式文件系统和高并发的批处理系统,但是它不适用于提供实时计算;
HBase是可以提供实时计算的分布式数据库,数据被保存在HDFS分布式文件系统上,由HDFS保证期高容错性;
但是再生产环境中,HBase是如何基于hadoop提供实时性呢?
HBase上的数据是以StoreFile(HFile)二进制流的形式存储在HDFS上block块儿中;
但是HDFS并不知道的HBase用于存储什么,它只把存储文件认为是二进制文件,也就是说,HBase的存储数据对于HDFS文件系统是透明的。
3、HBase与HDFS
在下面的表格中,我们对HDFS与HBase进行比较:
HDFS | HBase |
HDFS适于存储大容量文件的分布式文件系统。 | HBase是建立在HDFS之上的数据库。 |
HDFS不支持快速单独记录查找。 | HBase提供在较大的表快速查找 |
HDFS提供了高延迟批量处理;没有批处理概念。 | HBase提供了数十亿条记录低延迟访问单个行记录(随机存取)。 |
HDFS提供的数据只能顺序访问。 | HBase内部使用哈希表和提供随机接入,并且其存储索引,可将在HDFS文件中的数据进行快速查找。 |
Hbase ---> HashMap(看作)
4、HBase相关概念
1、分布式数据库
2、列式存储
3、稀疏性
空缺的数据不存储 null 值,直接不占用空间
HBase中需要根据行键、列簇、列限定符和时间戳来确定一个单元格,因此,可以视为一个“四维坐标”,即[行键, 列簇, 列限定符, 时间戳]
4、数据模型
HBase通过表格的模式存储数据,每个表格由列和行组成,其中,每个列又被划分为若干个列簇(colnum family),请参考下面的图:
表:HBase的数据同样是用表来组织的,表由行和列组成,列分为若干个列簇,行和列的坐标交叉决定了一个单元格。
行键:每个表由若干行组成,每个行有一个行键作为这一行的唯一标识。访问表中的行只有三种方式:通过单个行键进行查询、通过一个行键的区间来访问、全表扫描。
列簇:一个HBase表被分组成许多“列簇”的集合,它是基本的访问控制单元。
列修饰符(列限定符):列簇里的数据通过列限定符(或列)来定位
单元格:在HBase表中,通过行、列簇和列限定符确定一个“单元格”(cell),单元格中存储的数据没有数据类型,总被视为字节数组byte[]
时间戳:每个单元格都保存着同一份数据的多个版本,这些版本采用时间戳进行索引
5、HBase数据坐标
HBase中需要根据行键、列簇、列限定符和时间戳来确定一个单元格(cell),cell中的数据是没有类型的,全部是字节码形式存贮。,因此,可以视为一个“四维坐标”,即[行键, 列簇, 列限定符, 时间戳]。
对于上图这样一个HBase表,其数据坐标举例如下:
键 | 值 |
[“201505003”, “Info”, “email”, 1174184619081] | “xie@qq.com” |
[“201505003”, “Info”, “email”, 1174184620720] | “you@163.com” |
6、HBase区域
HBase自动把表水平划分为区域(Region),每个区域都是有若干连续行构成的,一个区域由所属的表、起始行、终止行(不包括这行)三个要素来表示。
一开始,一个表只有一个区域,但是随着数据的增加,区域逐渐变大,等到它超出设定的阈值(128M)大小,就会在某行的边界上进行拆分,分成两个大小基本相同的区域。然后随着数据的再增加,区域就不断的增加,如果超出了单台服务器的容量,就可以把一些区域放到其他节点上去,构成一个集群。也就是说:集群中的每个节点(Region Server)管理整个表的若干个区域。所以,我们说:区域是HBase集群上分布数据的最小单位。
5、HBase系统架构
架构图
组件介绍
HBase由三种类型的服务器以主从模式构成:
- Region Server:负责数据的读写服务,用户通过与Region server交互来实现对数据的访问。
- HBase HMaster:负责Region的分配及数据库的创建和删除等操作。
- ZooKeeper:负责维护集群的状态(某台服务器是否在线,服务器之间数据的同步操作及master的选举等)。
HDFS的DataNode负责存储所有Region Server所管理的数据,即HBase中的所有数据都是以HDFS文件的形式存储的。出于使Region server所管理的数据更加本地化的考虑,Region server是根据DataNode分布的。HBase的数据在写入的时候都存储在本地。但当某一个region被移除或被重新分配的时候,就可能产生数据不在本地的情况。这种情况只有在所谓的compaction之后才能解决。
Client
包含访问HBase的接口并维护cache来加快对HBase的访问
Zookeeper
保证任何时候,集群中只有一个master
存贮所有Region的寻址入口。
实时监控Region server的上线和下线信息。并实时通知Master
存储HBase的schema和table元数据的meta信息
Master
为Region server分配region
负责Region server的负载均衡
发现失效的Region server并重新分配其上的region
管理用户对table的增删改操作
RegionServer
Region server维护region,处理对这些region的IO请求
Region server负责切分在运行过程中变得过大的region
HLog(WAL log):
HLog文件就是一个普通的Hadoop Sequence File,Sequence File 的Key是 HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和 region名字外,同时还包括sequence number和timestamp,timestamp是” 写入时间”,sequence number的起始值为0,或者是最近一次存入文件系 统sequence number。
HLog SequeceFile的Value是HBase的KeyValue对象,即对应HFile中的 KeyValue
Region
HBase自动把表水平划分成多个区域(region),每个region会保存一个表里面某段连续的数据;每个表一开始只有一个region,随着数据不断插 入表,region不断增大,当增大到一个阀值的时候,region就会等分会两个新的region(裂变);
当table中的行不断增多,就会有越来越多的region。这样一张完整的表被保存在多个Regionserver上。
Memstore 与 storefile
- 一个region由多个store组成,一个store对应一个CF(列簇)
- store包括位于内存中的memstore和位于磁盘的storefile写操作先写入 memstore,当memstore中的数据达到某个阈值,hregionserver会启动 flashcache进程写入storefile,每次写入形成单独的一个storefile
- 当storefile文件的数量增长到一定阈值后,系统会进行合并(minor、 major compaction),在合并过程中会进行版本合并和删除工作 (majar),形成更大的storefile。
- 当一个region所有storefile的大小和超过一定阈值后,会把当前的region 分割为两个,并由hmaster分配到相应的regionserver服务器,实现负载均衡。
- 客户端检索数据,先在memstore找,找不到再找storefile
- HRegion是HBase中分布式存储和负载均衡的最小单元。最小单元就表示不同的HRegion可以分布在不同的HRegion server上。
- HRegion由一个或者多个Store组成,每个store保存一个columns family。
- 每个Strore又由一个memStore和0至多个StoreFile组成。
如图:StoreFile 以HFile格式保存在HDFS上。
Table => Region => Store => memStor 和 StoreFile
理解难点
1、flush刷新在HDFS上呈现究竟是怎么刷新的呢??
我们目前刚刚学习的时候,添加数据,都是一条一条的put进去,而我们在put的数据比较少(小于128M)的时候,我们put完去HDFS上并未查看到我们put的文件,这是因为数据还在内存中,也就是还在memStore中,所以要想在HDFS中查看到,我们必须手动刷新到磁盘中,这是将memStore的数据刷新到StoreFile中去,这样我们在HDFS中就可以查看到了。
2、为什么Hbase不可以使用像Mysql那样进行查询??
首先,我们应该可以感受到,我们在插入的时候,每行数据,有多少列,列名叫什么完全是我们自己定义的,之所以不支持像MySql那样对列进行查询和操作,因为不确定列的个数和名称。
3、数据最后存在HDFS上的,HDFS不支持删改,为什么Hbase就可以呢??
这里有个思想误区,的确,数据是以HFile形式存在HDFS上的,而且HDFS的确是不支持删改的,但是为什么Hbase就支持呢?首先,这里的删除并不是真正意义上的对数据进行删除,而是对数据进行打上标记,我们再去查的时,就不会查到这个打过标记的数据,这个数据Hmaster会每隔1小时清理。修改是put两次,Hbase会取最新的数据,过期数据也是这个方式被清理。
2、HBase2.2.7安装搭建
1、上传解压配置环境变量
# 1、解压
tar -xvf hbase-2.2.7-bin.tar.gz.gz
# 2、配置环境变量
vim /etc/profile
# 3、在最后增加配置
export HBASE_HOME=/usr/local/soft/hbase-2.2.7
export PATH=$PATH:$HBASE_HOME/bin
# 4、使环境变量剩下
source /etc/profile
2、修改配置文件
#1、修改hbase-site.xml文件
vim hbase-site.xml
# 增加以下配置
<!--指定 zookeeper 服务器 -->
<property>
<name>hbase.zookeeper.quorum</name>
<value>master,node1,node2</value>
</property>
<!--指定 hbase 根路径 -->
<property>
<name>hbase.rootdir</name>
<value>hdfs://master:9000/hbase</value>
</property>
<!--将 hbase 设置为分布式部署。 -->
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<!-- 避免出现启动错误。 -->
<property>
<name>hbase.unsafe.stream.capability.enforce</name>
<value>false</value>
</property>
#2、修改hbase-env.sh文件
vim hbase-env.sh
# 增加配置
export HBASE_MANAGES_ZK=false
export JAVA_HOME=/opt/module/jdk1.8
#3、修改regionservers文件
vim regionservers
# 增加配
node1
node2
#4、同步到所有节点
scp -r hbase-1.4.6 node1:`pwd`
scp -r hbase-1.4.6 node2:`pwd`
3、启动Hbase集群
# 启动(在master上执行)
start-hbase.sh
# hbase web ui
http://master:16010
# 关闭hbase启动
#stop-hbase.sh
#连接服务端
hbase shell
4、解决 log4j 兼容性问题(非必要)
警告信息,不影响使用 LF4J: Class path contains multiple SLF4J bindings.
cd /usr/local/soft/hbase-2.2.7/lib/client-facing-thirdparty
# HBase 与 Hadoop 在运行时会出现 log4j 兼容性问题,这是因为 HBase 的 log4j 版本与 Hadoop 的产生了冲突,我们这里将 HBase 的 log4j 设置为备份。
mv slf4j-log4j12-1.7.25.jar slf4j-log4j12-1.7.25.jar.bak
5、重置HBase
1、关闭hbase集群
1)杀死进程
2)stop-hbase.sh
2、删除数据 hdfs
hdfs dfs -rmr /hbase
3、删除元数据 zk
zkCli.sh rmr /hbase
4、重新启动hbase
start-hbase.sh
时间同步
yum install ntp -y ntpdate -u time.windows.com
3、shell命令
命名 | 描述 | 语法 |
help ‘命名名’ | 查看命令的使用描述 | help ‘命令名’ |
whoami | 我是谁 | whoami |
version | 返回hbase版本信息 | version |
status | 返回hbase集群的状态信息 | status |
table_help | 查看如何操作表 | table_help |
create | 创建表 | create ‘表名’, ‘列簇名1’, ‘列簇名2’, ‘列簇名N’ |
alter | 修改列簇 | 添加一个列簇:alter ‘表名’, ‘列簇名’ 删除列簇:alter ‘表名’, {NAME=> ‘列簇名’, METHOD=> ‘delete’} |
desc/describe | 显示表相关的详细信息 | describe ‘表名’ |
list | 列出hbase中存在的所有表 | list |
exists | 测试表是否存在 | exists ‘表名’ |
put | 添加或修改的表的值 | put ‘表名’, ‘行键’, ‘列簇名’, ‘列值’ put ‘表名’, ‘行键’, ‘列簇名:列名’, ‘列值’ |
scan | 通过对表的扫描来获取对用的值 | scan ‘表名’ 扫描某个列簇: scan ‘表名’, {COLUMN=>‘列簇名’} 扫描某个列簇的某个列: scan ‘表名’, {COLUMN=>‘列簇名:列名’} 查询同一个列簇的多个列: scan ‘表名’, {COLUMNS => [ ‘列簇名1:列名1’, ‘列簇名1:列名2’, …]} |
get | 获取行或单元(cell)的值 | get ‘表名’, ‘行键’ get ‘表名’, ‘行键’, ‘列簇名’ |
count | 统计表中行的数量 | count ‘表名’ |
incr | 增加指定表行或列的值 | incr ‘表名’, ‘行键’, ‘列簇:列名’, 步长值 |
get_counter | 获取计数器 | get_counter ‘表名’, ‘行键’, ‘列簇:列名’ |
delete | 删除指定对象的值(可以为表,行,列对应的值,另外也可以指定时间戳的值) | 删除列簇的某个列: delete ‘表名’, ‘行键’, ‘列簇名:列名’ |
deleteall | 删除指定行的所有元素值 | deleteall ‘表名’, ‘行键’ |
truncate | 重新创建指定表 | truncate ‘表名’ |
enable | 使表有效 | enable ‘表名’ |
is_enabled | 是否启用 | is_enabled ‘表名’ |
disable | 使表无效 | disable ‘表名’ |
is_disabled | 是否无效 | is_disabled ‘表名’ |
drop | 删除表 | drop的表必须是disable的 disable ‘表名’ drop ‘表名’ |
shutdown | 关闭hbase集群(与exit不同) | |
tools | 列出hbase所支持的工具 | |
exit | 退出hbase shell |
详解
1、general 类
status; 显示集群状态
version; 查询数据库版本和时间等信息
whoami; 显示当前用户及用户组
table_help; 查看操作表的命令
exit; 退出shell客户端
2、DDL数据库定义语言
#创建表
#只创建表和列簇(最简表结构,每一个大括号写一个列簇)
create '表名', {NAME => '列簇1'},{NAME => '列簇2'},...
#简写(不加大括号和NAME关键字)
create '表名','列簇1','列簇2','列簇3'...
#最完整写法
#TTL秒级,默认forever。BLOCKCHE是否开启缓存,默认true。
create '表名',{NAME => '列簇1',VERSIONS => 版本个数,TTL => 过期时间,BLOCKCHE => bool}, {...}, ...
#修改表(增加/删除列簇和其他,重命名等操作不能执行)
#添加一个列簇
alter '表名','列簇名'
alter 't1','list1'
#删除一个列簇
alter '表名',{NAME => '列簇名',METHOD=>'delete'}
alter 't1',{NAME => 'list1', METHOD => 'delete'}
#修改列簇的属性(可以修改VERSIONS,)
# 修改f1列族的版本为5
alter 't1', NAME => 'f1', VERSIONS => 5
# 修改多个列族,修改f2为内存,版本号为5
alter 't1', 'f1', {NAME => 'f2', IN_MEMORY => true}, {NAME => 'f3', VERSIONS => 5}
# 也可以修改table-scope属性,例如MAX_FILESIZE, READONLY,MEMSTORE_FLUSHSIZE, DEFERRED_LOG_FLUSH等。
# 例如,修改region的最大大小为128MB:
alter 't1', MAX_FILESIZE => '134217728'
#获取表的描述desc/describe
desc '表名'
#列举所有表
list
#判断表是否存在exists
exists '表名'
#启用表和禁用表
#启用表
enable '表名'
#禁用表
disable '表名'
#判断表是否处于可用状态
is_enabled '表名'
#判断表是否处于不可用状态
is_disabled '表名'
#禁用满足正则表达式的所有表
disable_all '表达式'
disable_all 't.*' #禁用所有以t开头的表
#启用满足正则表达式的所有表
enable_all '表达式'
#删除表(先禁用,在删除)
disable '表名'
drop '表名'
#删除满足正则表达式的所有表drop_all
drop_all '表达式'
#获取rowKey所在的区locate_region
locate_region '表名','行键'
#显示hbase所支持的所有过滤器 show_filters
3、namespace
列举所有命名空间
list_namespace
描述namespace
describe_namespace '命名空间'
查看命名空间下的所有表
list_namespace_tables '命名空间'
创建命名空间
create_namespace '命名空间'
删除命名空间
drop_namespace '命名空间'
4、DML数据库操作语言
增/改put
删delete/deleteall/truncate
查get/scan
#自增
# 语法
incr '表名', '行键', '列族:列名', 步长值
# 示例
# 注意:incr 可以对不存的行键操作,如果行键已经存在会报错,如果使用put修改了incr的值再使用incr也会报错
# ERROR: org.apache.hadoop.hbase.DoNotRetryIOException: Field is not a long, it's 2 bytes wide
incr 'tbl_user', 'xiaohong', 'info:age', 1
#计数器get_counter获取自增列的值(get无法获取具体值)
get '表名', '行键', '列族:列名'
5、其他
#一次增加多列
scan '表名', {COLUMNS => [ '列族名1:列名1', '列族名1:列名2', ...]} ...
scan 'tbl', {COLUMNS => [ 'info:id', 'info:age']}
#TIMESTAMP指定时间戳
scan '表名',{TIMERANGE=>[timestamp1, timestamp2]}
scan 'tbl_user',{TIMERANGE=>[1551938004321, 1551938036450]}
#VERSIONS
#默认情况下一个列只能存储一个历史版本
#创建时指定
create '表名', {NAME=>'列簇', VERSIONS => 个数}
#建表后添加或修改
alter '表名',{NAME=>'列簇', VERSIONS => 个数}
#起始列和戒指列
scan '表名',{STARTROW => '列簇名'};查看指定列簇后的所有列簇数据。按字典排序
scan '表名',{STOPROW => '列簇'};查看指定列簇之前的所有数据
限制查看行数LIMIT
scan '表名',{LIMIT => 行数}
查询结果反转REVERSED
scan 'teacher',{REVERSED => true}
6、过滤器
1、ValueFilter:值过滤器
#查询列等于给定值得数据
scan '表名',FILTER=>"ValueFilter(=,'binary:列值')"
#查询列包含指定值得数据
scan '表名',FILTER=>"ValueFilter(=,'substring:列值')"
#示例
#查询值等于26得数据
scan 'tb1',FILTER=>"ValueFilter(=,'binary:26')"
#查询值包含6得数据
scan 'tb1',FILTER=>"ValueFilter(=,'substring:6')"
2、ColumnPrefixFilter:列名过滤器
#查询指定前缀得数据,相当于startWiths
scan '表名',FILTER=>"ColumnPrefixFilter('前缀')"
# 示例
scan 'tbl_user', FILTER=>"ColumnPrefixFilter('birth')"
# 通过括号、AND和OR的条件组合多个过滤器
scan 'tbl_user', FILTER=>"ColumnPrefixFilter('birth') AND ValueFilter(=,'substring:26')"
其他略
4、Region相关
刷新数据:flush 'tb'
合并数据:major_compact 'tb'
1、Region信息观察
1.1 查看表的所有region
list_regions '表名'
1.2 强制将表切分出来一个region
split '表名','行键'
但是在页面上可以看到三个:过一会会自动的把原来的删除
1.3 查看某一行在哪个region中
locate_region '表名','行键'
可以hbase hfile -p -f xxxx 查看
2、预分region解决热点问题(面试题)
1.热点现象
检索habse的记录首先要通过row key来定位数据行,当大量的client访问hbase集群的一个或少数几个节点,
造成少数region server的读/写请求过多、负载过大,而其他region server负载却很小,就造成了“热点”现象.
大量访问会使热点region所在的单个主机负载过大,引起性能下降甚至region不可用。
2.产生原因
有大量连续编号的row key ==> 大量row key相近的记录集中在个别region ==> client检索记录时,对个别region访问过多 ==> 此region所在的主机过载 ==> 热点
3、解决方法
1、预分区
2、加盐
3、制定合适的rowKey
预分区详解
(在建表的时候根据具体的查询业务 设计rowkey 预拆分)
在默认的拆分策略中 ,region的大小达到一定的阈值以后才会进行拆分,并且拆分的region在同一个regionserver中 ,只有达到负载均衡的时机时才会进行region重分配!并且开始如果有大量的数据进行插入操作,那么并发就会集中在单个RS中, 形成热点问题,所以如果有并发插入的时候尽量避免热点问题 ,应当预划分 Region的rowkeyRange范围 ,在建表的时候就指定预region范围
查看命令使用(指定4个切割点,就会有5个region)
help 'create'
create 'tb_split','cf',SPLITS => ['e','h','l','r']
list_regions 'tb_split'
添加数据试试
put 'tb_split','c001','cf:name','first'
put 'tb_split','f001','cf:name','second'
put 'tb_split','z001','cf:name','last'
hbase hfile -p --f xxxx 查看数据
如果没有数据,因为数据还在内存中,需要手动刷新内存到HDFS中,以HFile的形式存储
3、日志查看
演示不启动hdfs 就启动hbase
日志目录:
/usr/local/soft/hbase-1.7.1/logs
start-all.sh发现HMaster没启动,hbase shell客户端也可以正常访问再启动hbase就好了
5、命令进阶
1、scan进阶使用
查看所有的命名空间
list_namespace
查看某个命名空间下的所有表
list_namespace_tables 'default'
修改命名空间,设置一个属性
alter_namespace 'bigdata25',{METHOD=>'set','author'=>'wyh'}
查看命名空间属性
describe_namespace 'bigdata17'
删除一个属性
alter_namespace 'bigdata17',{METHOD=>'unset', NAME=>'author'}
删除一个命名空间
drop_namespace 'bigdata17'
创建一张表
create 'teacher','cf'
添加数据
put 'teacher','tid0001','cf:tid',1
put 'teacher','tid0002','cf:tid',2
put 'teacher','tid0003','cf:tid',3
put 'teacher','tid0004','cf:tid',4
put 'teacher','tid0005','cf:tid',5
put 'teacher','tid0006','cf:tid',6
显示三行数据
scan 'teacher',{LIMIT=>3}
从后查三行
scan 'teacher',{LIMIT=>3,REVERSED=>true}
查看包含指定列的行
scan 'teacher',{LIMIT=>3,COLUMNS=>['cf:name']}
简化写法:
scan 'teacher',LIMIT=>3,COLUMNS=>['cf:name']
在已有的值后面追加值
append 'teacher','tid00001','cf:name','123'
2、get进阶使用
简单使用,获取某一行数据
get 'teacher','tid0001'
获取某一行的某个列簇
get 'teacher','tid0001','cf'
获取某一行的某一列(属性 )
get 'teacher','tid0001','cf:name'
可以新增一个列簇数据测试
查看历史版本1、修改表可以存储多个版本
alter 'teacher',NAME=>'cf',VERSIONS=>3
2、put四次相同rowkey和列的数据
put 'teacher','tid0001','cf:name','xiaohu1'
put 'teacher','tid0001','cf:name','xiaohu2'
put 'teacher','tid0001','cf:name','xiaohu3'
put 'teacher','tid0001','cf:name','xiaohu4'
3、查看历史数据,默认是最新的
get 'teacher','tid0001',COLUMN=>'cf:name',VERSIONS=>2
修改列簇的过期时间 TTL单位是秒,这个时间是与插入的时间比较,而不是现在开始60s
alter 'teacher',{NAME=>'cf2',TTL=>'60'}
3、插入时间指定时间戳
put 'teacher','tid0007','cf2:job','bigdata25',1693644893843
6、HBase读写流程
1、读流程
Hbase读取数据的流程:
1)是由客户端发起读取数据的请求,首先会与zookeeper建立连接
2)从zookeeper中获取一个hbase:meta表位置信息,被哪一个regionserver所管理着
hbase:meta表:hbase的元数据表,在这个表中存储了自定义表相关的元数据,包括表名,表有哪些列簇,表有哪些region,每个region存储的位置,每个region被哪个regionserver所管理,这个表也是存储在某一个region上的,并且这个meta表只会被一个regionserver所管理。这个表的位置信息只有zookeeper知道。
3)连接这个meta表对应的regionserver,从meta表中获取当前你要读取的这个表对应的regionsever是谁。
当一个表多个region怎么办呢?
如果我们获取数据是以get的方式,只会返回一个regionserver
如果我们获取数据是以scan的方式,会将所有的region对应的regionserver的地址全部返回。
4)连接要读取表的对应的regionserver,从regionserver上的开始读取数据:
读取顺序:memstore-->blockcache-->storefile-->Hfile中
注意:如果是scan操作,就不仅仅去blockcache了,而是所有都会去找。
2、写流程
--------------------------1-4步是客户端写入数据的流程-----------------
Hbase的写入数据流程:
1)由客户端发起写数据请求,首先会与zookeeper建立连接
2)从zookeeper中获取hbase:meta表被哪一个regionserver所管理
3)连接hbase:meta表中获取对应的regionserver地址 (从meta表中获取当前要写入数据的表对应的region所管理的regionserver) 只会返回一个regionserver地址
4)与要写入数据的regionserver建立连接,然后开始写入数据,将数据首先会写入到HLog,然后将数据写入到对应store模块中的memstore中
(可能会写多个),当这两个地方都写入完成之后,表示数据写入完成。
-------------------------后面的步骤是服务器内部的操作-----------------
异步操作
5)随着客户端不断地写入数据,memstore中的数据会越来多,当内存中的数据达到阈值(128M/1h)的时候,放入到blockchache中,生成新的memstore接收用户过来的数据,然后当blockcache的大小达到一定阈值(0.85)的时候,开始触发flush机制,将数据最终刷新到HDFS中形成小的Hfile文件。
6)随着不断地刷新,storefile不断地在HDFS上生成小HFIle文件,当小的HFile文件达到阈值的时候(3个及3个以上),就会触发Compaction机制,将小的HFile合并成一个大的HFile.
7)随着不断地合并,大的HFile文件会越来越大,当达到一定阈值(2.0版本之后最终10G)的时候,会触发分裂机制(split),将大的HFile文件进行一分为二,同时管理这个大的HFile的region也会被一分为二,形成两个新的region和两个新的HFile文件,一对一的进行管理,将原来旧的region和分裂之前大的HFile文件慢慢地就会下线处理。
3、面对百亿数据,HBase为什么查询速度依然非常快?(面试题)
roeKey范围 => region => 列簇 => HFile => key => value
10T => 2G => 500M => 250M => 10M => 0.1秒内找到
HBase适合存储PB级别的海量数据(百亿千亿量级条记录),如果根据记录主键Rowkey来查询,能在几十到百毫秒内返回数据。
那么HBase是如何做到的呢?
接下来,简单阐述一下数据的查询思路和过程。
查询过程
第1步:
项目有100亿业务数据,存储在一个HBase集群上(由多个服务器数据节点构成),每个数据节点上有若干个Region(区域),每个Region实际上就是HBase中一批数据的集合(一段连续范围rowkey的数据)。
我们现在开始根据主键RowKey来查询对应的记录,通过meta表可以帮我们迅速定位到该记录所在的数据节点,以及数据节点中的Region,目前我们有100亿条记录,占空间10TB。所有记录被切分成5000个Region,那么现在,每个Region就是2G。
由于记录在1个Region中,所以现在我们只要查询这2G的记录文件,就能找到对应记录。
第2步:
由于HBase存储数据是按照列族存储的。比如一条记录有400个字段,前100个字段是人员信息相关,这是一个列簇(列的集合);中间100个字段是公司信息相关,是一个列簇。另外100个字段是人员交易信息相关,也是一个列簇;最后还有100个字段是其他信息,也是一个列簇
这四个列簇是分开存储的,这时,假设2G的Region文件中,分为4个列族,那么每个列族就是500M。
到这里,我们只需要遍历这500M的列簇就可以找到对应的记录。
第3步:
如果要查询的记录在其中1个列族上,1个列族在HDFS中会包含1个或者多个HFile。
如果一个HFile一般的大小为100M,那么该列族包含5个HFile在磁盘上或内存中。
由于HBase的内存进入磁盘中的数据是排好序(字典顺序)的,要查询的记录有可能在最前面,也有可能在最后面,按平均来算,我们只需遍历2.5个HFile共250M,即可找到对应的记录。
第4步:
每个HFile中,是以键值对(key/value)方式存储,只要遍历文件中的key位置并判断符合条件即可
一般key是有限的长度,假设key/value比是1:24,最终只需要10M的数据量,就可获取的对应的记录。
如果数据在机械磁盘上,按其访问速度100M/S,只需0.1秒即可查到。
如果是SSD的话,0.01秒即可查到。
当然,扫描HFile时还可以通过布隆过滤器快速定位到对应的HFile,以及HBase是有内存缓存机制的,如果数据在内存中,效率会更高
总结
正因为以上大致的查询思路,保证了HBase即使随着数据量的剧增,也不会导致查询性能的下降。
同时,HBase是一个面向列存储的数据库(列簇机制),当表字段非常多时,可以把其中一些字段独立出来放在一部分机器上,而另外一些字段放到另一部分机器上,分散存储,分散列查询。
正由于这样复杂的存储结构和分布式的存储方式,保证了HBase海量数据下的查询效率。
7、JavaAPI操作HBase
1、导入依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.2.7</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-client</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
<version>3.1.1</version>
</dependency>
</dependencies>
2、JavaAPI实现增删改查
1、对命名空间操作
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Admin;
import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.client.ConnectionFactory;
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 NameSpaceTest {
private Connection connection = null;
private Admin admin = null;
@Before
public void initTest() throws IOException {
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "master:2181,node1:2181,node2:2181");
connection = ConnectionFactory.createConnection(conf);
admin = connection.getAdmin();
}
//创建命名空间
@Test
public void createNameSpace() throws IOException {
NamespaceDescriptor.Builder builder = NamespaceDescriptor.create("test");
admin.createNamespace(builder.build());
}
//删除命名空间,需要先删除所有表才能删除命名空间
@Test
public void dropNameSpace() throws IOException {
//获取命名空间得所有的表
TableName[] javaAPIS = admin.listTableNamesByNamespace("javaAPI");
for (TableName javaAPI : javaAPIS) {
System.out.println(Bytes.toString(javaAPI.getName()));
//禁用表
admin.disableTable(javaAPI);
//删除表
admin.deleteTable(javaAPI);
}
//删除命名空间
admin.deleteNamespace("javaAPI");
}
//查看所有命名空间
@Test
public void listNameSpace() throws IOException {
NamespaceDescriptor[] listNamespaces = admin.listNamespaceDescriptors();
for (NamespaceDescriptor listNamespace : listNamespaces) {
String name = listNamespace.getName();
System.out.println(name);
}
}
//描述namespace
@Test
public void descNameSpace() throws IOException {
NamespaceDescriptor test = admin.getNamespaceDescriptor("test");
System.out.println(test.toString());
}
@After
public void closeTest() throws IOException {
admin.close();
connection.close();
}
}
2、对表结构操作
import org.apache.hadoop.conf.Configuration;
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.Arrays;
public class TableTest {
private Connection connection;
private Admin admin;
@Before
public void initTest() throws IOException {
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "master:2181,node1:2181,node2:2181");
connection = ConnectionFactory.createConnection(conf);
admin = connection.getAdmin();
}
//创建表
@Test
public void createTable() throws IOException {
//创建TableName对象
TableName test1 = TableName.valueOf("test:test1");
//使用TableDescriptorBuilder创建TableDescriptor
TableDescriptorBuilder newBuilder = TableDescriptorBuilder.newBuilder(test1);
//使用ColumnFamilyDescriptorBuilder创建列簇ColumnFamilyDescriptor信息
ColumnFamilyDescriptorBuilder column1 = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("column1"));
//给表添加列簇信息
TableDescriptorBuilder tableDescriptorBuilder = newBuilder.setColumnFamily(column1.build());
//利用方法创建表,需要TableDescriptor类型得函数
admin.createTable(tableDescriptorBuilder.build());
}
//删除表。先禁用、在删除
@Test
public void dropTable() throws IOException {
TableName test1 = TableName.valueOf("test1");
admin.disableTable(test1);//禁用表
admin.deleteTable(test1);
}
//修改表
@Test
public void changeTable() throws IOException {
//添加一个列簇
TableName tableName = TableName.valueOf("test:test1");
// ColumnFamilyDescriptor test2 = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("column2")).build();
// admin.addColumnFamily(tableName,test2);
//删除一个列簇
// admin.deleteColumnFamily(tableName,Bytes.toBytes("test2"));
//修改列簇得属性
ColumnFamilyDescriptorBuilder column2 = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("column2"));
column2.setMaxVersions(5);//设置最大版本数5个
column2.setTimeToLive(60 * 60 * 24 * 60);//设置列簇存活时间,秒级
ColumnFamilyDescriptor build = column2.build();
admin.modifyColumnFamily(tableName, build);
}
//获取表信息,(查)
@Test
public void descTable() throws IOException {
TableName tableName = TableName.valueOf("test:test1");
Table table = connection.getTable(tableName);
TableDescriptor descriptor = table.getDescriptor();
System.out.println(descriptor.toString());
}
//获取所有表
@Test
public void listTable() throws IOException {
TableName[] tables = admin.listTableNames();
System.out.println(Arrays.toString(tables));
//获取命名空间下的所有表
TableName[] tableByNamespaces = admin.listTableNamesByNamespace("test");
System.out.println(Arrays.toString(tableByNamespaces));
}
//判断表是否存在
@Test
public void isExists() throws IOException {
TableName table1 = TableName.valueOf("test:test1");
TableName table2 = TableName.valueOf("test:test2");
boolean t1 = admin.tableExists(table1);
boolean t2 = admin.tableExists(table2);
System.out.println("test:test1 is exitst => " + t1);
System.out.println("test:test2 is exitst => " + t2);
}
@After
public void cloceTest() throws IOException {
admin.close();
connection.close();
}
}
3、对数据进行操作
import org.apache.hadoop.conf.Configuration;
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.Arrays;
import java.util.Collections;
public class DMLTest {
private Table table;
private Connection connection;
@Before
public void initTest() throws IOException {
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "master:2181,node1:2181,node2:2181");
connection = ConnectionFactory.createConnection(conf);
table = connection.getTable(TableName.valueOf("test:test1"));
}
//添加/修改数据
@Test
public void insetData() throws IOException {
//添加一条数据
// Put put = new Put(Bytes.toBytes("1001"));
// put.addColumn(Bytes.toBytes("column1"), Bytes.toBytes("name"), Bytes.toBytes("alice"));
// table.put(put);
//添加多条数据
ArrayList<Put> puts = new ArrayList<>();
Put data1 = new Put(Bytes.toBytes("1001"));
data1.addColumn(Bytes.toBytes("column2"), Bytes.toBytes("name"), Bytes.toBytes("alice"));
Put data2 = new Put(Bytes.toBytes("1001"));
data2.addColumn(Bytes.toBytes("column2"), Bytes.toBytes("age"), Bytes.toBytes("18"));
Put data3 = new Put(Bytes.toBytes("1001"));
data3.addColumn(Bytes.toBytes("column2"), Bytes.toBytes("sex"), Bytes.toBytes("nan"));
Put data4 = new Put(Bytes.toBytes("1001"));
data4.addColumn(Bytes.toBytes("column2"), Bytes.toBytes("phone"), Bytes.toBytes("110119"));
Collections.addAll(puts, data1, data2, data3, data4);
table.put(puts);
}
//删除数据
@Test
public void deleteData() throws IOException {
//删除一条数据
// Delete delete = new Delete(Bytes.toBytes("1001"));
// delete.addColumn(Bytes.toBytes("column2"),Bytes.toBytes("phone"));
// table.delete(delete);
//删除多条数据
ArrayList<Delete> deletes = new ArrayList<>();
Delete delete1 = new Delete(Bytes.toBytes("1001"));
delete1.addColumn(Bytes.toBytes("column2"), Bytes.toBytes("age"));
Delete delete2 = new Delete(Bytes.toBytes("1001"));
delete2.addColumn(Bytes.toBytes("column2"), Bytes.toBytes("sex"));
deletes.add(delete1);
deletes.add(delete2);
table.delete(deletes);
}
//查询数据
@Test
public void getAndScan() throws IOException {
//get查询
// Get get = new Get(Bytes.toBytes("1001"));
// get.addFamily(Bytes.toBytes("column1"));
// Result result = table.get(get);
// Cell[] cells = result.rawCells();
// for (Cell cell : cells) {
// byte[] bytes = CellUtil.cloneQualifier(cell);
// byte[] bytes1 = CellUtil.cloneValue(cell);
// System.out.println(Bytes.toString(bytes) + ": " + Bytes.toString(bytes1));
// }
//scan查询
Scan scan = new Scan();
scan.setId("1001");
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] bytes = CellUtil.cloneQualifier(cell);
byte[] bytes1 = CellUtil.cloneValue(cell);
System.out.println(Bytes.toString(bytes) + ": " + Bytes.toString(bytes1));
}
}
}
@After
public void closeTest() throws IOException {
table.close();
connection.close();
}
}
3、过滤器详解
HBase 的基本 API,包括增、删、改、查等。增、删都是相对简单的操作,与传统的 RDBMS 相比,这里的查询操作略显苍白,只能根据特性的行键进行查询(Get)或者根据行键的范围来查询(Scan)。HBase 不仅提供了这些简单的查询,而且提供了更加高级的过滤器(Filter)来查询。
过滤器可以根据列族、列、版本等更多的条件来对数据进行过滤,
基于 HBase 本身提供的三维有序(行键,列,版本有序),这些过滤器可以高效地完成查询过滤的任务,带有过滤器条件的 RPC 查询请求会把过滤器分发到各个 RegionServer(这是一个服务端过滤器),这样也可以降低网络传输的压力。
使用过滤器至少需要两类参数:
一类是抽象的操作符,另一类是比较器
作用
- 过滤器的作用是在服务端判断数据是否满足条件,然后只将满足条件的数据返回给客户端
- 过滤器的类型很多,但是可以分为三大类:
- 比较过滤器:可应用于rowkey、列簇、列、列值过滤器
- 专用过滤器:只能适用于特定的过滤器
- 包装过滤器:包装过滤器就是通过包装其他过滤器以实现某些拓展的功能。
比较过滤器
所有比较过滤器均继承自 CompareFilter。创建一个比较过滤器需要两个参数,分别是比较运算符和比较器实例。
public CompareFilter(final CompareOp compareOp,final ByteArrayComparable comparator) {
this.compareOp = compareOp;
this.comparator = comparator;
}
比较运算符
- LESS
- LESS_OR_EQUAL
- EQUAL =
- NOT_EQUAL <>
- GREATER_OR_EQUAL >=
- GREATER >
- NO_OP 排除所有
常见的六大比较器
BinaryComparator (常用)
按字节索引顺序比较指定字节数组,采用Bytes.compareTo(byte[])
BinaryPrefixComparator (常用)
通BinaryComparator,只是比较左端前缀的数据是否相同
NullComparator
判断给定的是否为空
BitComparator
按位比较
RegexStringComparator
提供一个正则的比较器,仅支持 EQUAL 和非EQUAL
SubstringComparator (常用)
判断提供的子串是否出现在中
可对行键、列簇、列名、值进行过滤。分为:
- rowKey过滤器:RowFilter 行键过滤器
- 列簇过滤器:FamilyFilter
- 列名过滤器:QualifierFilter
- 列值过滤器:ValueFilter
比较过滤器代码演示
过滤器 ( 运算符, 比较器 );
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
import org.apache.hadoop.hbase.client.*;
import org.apache.hadoop.hbase.filter.*;
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.Arrays;
public class FilterTest {
private Connection connection;
private Table table;
@Before
public void initTest() throws IOException {
Configuration conf = new Configuration();
conf.set("hbase.zookeeper.quorum", "master:2181,node1:2181,node2:2181");
connection = ConnectionFactory.createConnection(conf);
table = connection.getTable(TableName.valueOf("students"));
}
//rowKey过滤器:RowFilter 行键过滤器
//通过RowFilter与BinaryComparator过滤比rowKey 1500100010小的所有值出来
@Test
public void rowFilterTest() throws IOException {
Scan scan = new Scan();
BinaryComparator binaryComparator = new BinaryComparator(Bytes.toBytes("1500100010"));
RowFilter rowFilter = new RowFilter(CompareOperator.LESS, binaryComparator);
scan.setFilter(rowFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
//列簇过滤器:FamilyFilter
// 通过FamilyFilter与SubstringComparator查询列簇名包含in的所有列簇下面的数据
@Test
public void FamilyFilterTest() throws IOException {
Scan scan = new Scan();
// SubstringComparator in = new SubstringComparator("in");
BinaryPrefixComparator i = new BinaryPrefixComparator(Bytes.toBytes("sex"));
// FamilyFilter familyFilter = new FamilyFilter(CompareOperator.EQUAL, i);
// scan.setFilter(familyFilter);
QualifierFilter qualifierFilter = new QualifierFilter(CompareOperator.EQUAL, i);
scan.setFilter(qualifierFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
@After
public void closeTest() throws IOException {
table.close();
connection.close();
}
}
专用过滤器
1、单列值过滤器:SingleColumnValueFilter (最常用)
SingleColumnValueFilter会返回满足条件的cell所在行的所有cell的值(即会返回一行数据)
2、列值排除过滤器:SingleColumnValueExcludeFilter
与SingleColumnValueFilter相反,会排除掉指定的列,其他的列全部返回
3、rowkey前缀过滤器:PrefixFilter
4、分页过滤器PageFilter
使用PageFilter分页效率比较低,每次都需要扫描前面的数据,直到扫描到所需要查的数据
可设计一个合理的rowkey来实现分页需求
代码示例:省去了初始化和关闭资源操作
//单列值过滤器:SingleColumnValueFilter
// SingleColumnValueFilter会返回满足条件的cell所在行的所有cell的值(即会返回一行数据)
// 通过SingleColumnValueFilter与查询文科班所有学生信息
@Test
public void singleFilterTest() throws IOException {
Scan scan = new Scan();
BinaryPrefixComparator prefixComparator = new BinaryPrefixComparator(Bytes.toBytes("文科"));
SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(Bytes.toBytes("info"), Bytes.toBytes("clazz"), CompareOperator.EQUAL, prefixComparator);
scan.setFilter(singleColumnValueFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
//列值排除过滤器:SingleColumnValueExcludeFilter
// 与SingleColumnValueFilter相反,会排除掉指定的列,其他的列全部返回
// 查询文科班所有学生信息,最终返回除clazz列的其他所有列
@Test
public void singleFilterExcludeTest() throws IOException {
Scan scan = new Scan();
BinaryPrefixComparator prefixComparator = new BinaryPrefixComparator(Bytes.toBytes("文科"));
SingleColumnValueFilter singleColumnValueExcludeFilter = new SingleColumnValueExcludeFilter(Bytes.toBytes("info"), Bytes.toBytes("clazz"), CompareOperator.EQUAL, prefixComparator);
scan.setFilter(singleColumnValueExcludeFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
//rowkey前缀过滤器:PrefixFilter
//通过PrefixFilter查询以15001001开头的所有前缀的rowkey
@Test
public void prefixFilterTest() throws IOException {
Scan scan = new Scan();
PrefixFilter prefixFilter = new PrefixFilter(Bytes.toBytes("150010005"));
scan.setFilter(prefixFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
//分页过滤器PageFilter
//使用PageFilter分页效率比较低,每次都需要扫描前面的数据,直到扫描到所需要查的数据
//可设计一个合理的rowkey来实现分页需求
@Test
public void pageFilterText() throws IOException {
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes("1500100050"));
PageFilter pageFilter = new PageFilter(10);
scan.setFilter(pageFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
/**
* 分页过滤器 需求:前30行分3页出来
* 通过给每一页的最后一行的行键加0,并把这个值重新传定义每页第一行的方法,作为下一页的开始行。达到下一页的目的。
* 具体代码在138,139两行。
*/
@Test
public void pageFilterFun() {
try {
//将表名封装成TableName的对象
TableName students2 = TableName.valueOf("students2");
//获取表的实例对象
Table table = conn.getTable(students2);
//定义你要查询的页数
int pageNum = 3;
//定义每一页的行数
int pageSize = 10;
//定义开始行数的变量
String currentPageStartRow = "";
//创建一个Scan对象
Scan scan = new Scan();
for (int i = 1; i <= pageNum; i++) {
System.out.println("================================当前是第"+i+"页=====================================");
//创建分页过滤器
PageFilter pageFilter = new PageFilter(pageSize);
scan.setFilter(pageFilter);
ResultScanner resultScanner = table.getScanner(scan);
// printResultScanner(resultScanner);
//获取到每一行
for (Result result : resultScanner) {
//获取当前行的rowkey+0作为下一页的开始行(最关键点)
currentPageStartRow = Bytes.toString(result.getRow())+0;
scan.withStartRow(Bytes.toBytes(currentPageStartRow));
//学号
String id = Bytes.toString(result.getRow());
String name = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("姓名")));
String age = Bytes.toString(result.getValue(Bytes.toBytes("info"), Bytes.toBytes("年龄")));
String gender = Bytes.toString(result.getValue(Bytes.toBytes("shujia"), Bytes.toBytes("性别")));
String clazz = Bytes.toString(result.getValue(Bytes.toBytes("shujia"), Bytes.toBytes("班级")));
System.out.println("学号:"+id+", 姓名:"+name+", 年龄:"+age+", 性别:"+gender+", 班级"+clazz);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
包装过滤器
1、SkipFilter过滤器
SkipFilter包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时,则拓展过滤整行数据。
2、WhileMatchFilter过滤器
WhileMatchFilter 包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时,WhileMatchFilter 则结束本次扫描,返回已经扫描到的结果。
//SkipFilter过滤器
//SkipFilter包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时,
// 则拓展过滤整行数据。下面是一个使用示例:
@Test
public void skipFilterTest() throws IOException {
Scan scan = new Scan();
BinaryComparator binaryComparator = new BinaryComparator(Bytes.toBytes("男"));
ValueFilter valueFilter = new ValueFilter(CompareOperator.NOT_EQUAL, binaryComparator);
SkipFilter skipFilter = new SkipFilter(valueFilter);
scan.setFilter(skipFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
//WhileMatchFilter过滤器
//WhileMatchFilter 包装一个过滤器,当被包装的过滤器遇到一个需要过滤的 KeyValue 实例时
// WhileMatchFilter 则结束本次扫描,返回已经扫描到的结果。下面是其使用示例:
@Test
public void whileMatchFilterTest() throws IOException {
Scan scan = new Scan();
BinaryComparator binaryComparator = new BinaryComparator(Bytes.toBytes("文科一班"));
ValueFilter valueFilter = new ValueFilter(CompareOperator.NOT_EQUAL, binaryComparator);
WhileMatchFilter whileMatchFilter = new WhileMatchFilter(valueFilter);
scan.setFilter(whileMatchFilter);
ResultScanner scanner = table.getScanner(scan);
Result next = null;
while ((next = scanner.next()) != null) {
StringBuilder sb = new StringBuilder();
byte[] id = next.getRow();
sb.append(Bytes.toString(id));
Cell[] cells = next.rawCells();
for (Cell cell : cells) {
byte[] column = CellUtil.cloneQualifier(cell);
byte[] value = CellUtil.cloneValue(cell);
sb.append(",").append(Bytes.toString(column)).append(":").append(Bytes.toString(value));
}
System.out.println(sb);
}
}
多个过滤器结合查询
以上都是讲解单个过滤器的作用,当需要多个过滤器共同作用于一次查询的时候,就需要使用 FilterList。FilterList 支持通过构造器或者 addFilter 方法传入多个过滤器。
//过滤器1
SingleColumnValueFilter filter1 = new SingleColumnValueFilter(
Bytes.toBytes("info"),
Bytes.toBytes("姓名"),
CompareOperator.EQUAL,
new BinaryPrefixComparator(Bytes.toBytes("于"))
);
//过滤器2
SingleColumnValueFilter filter2 = new SingleColumnValueFilter(
Bytes.toBytes("info"),
Bytes.toBytes("年龄"),
CompareOperator.LESS_OR_EQUAL,
new BinaryComparator(Bytes.toBytes("23"))
);
//过滤器3
SingleColumnValueFilter filter3 = new SingleColumnValueFilter(
Bytes.toBytes("shujia"),
Bytes.toBytes("性别"),
CompareOperator.EQUAL,
new SubstringComparator("女")
);
//过滤器4
SingleColumnValueFilter filter4 = new SingleColumnValueFilter(
Bytes.toBytes("shujia"),
Bytes.toBytes("班级"),
CompareOperator.EQUAL,
new RegexStringComparator("^理科")
);
//创建一个过滤器列表(最关键)
FilterList filterList = new FilterList();
//将每一个过滤器添加到列表中(最关键)
filterList.addFilter(filter1);
filterList.addFilter(filter2);
filterList.addFilter(filter3);
filterList.addFilter(filter4);
//将这个过滤器集合添加到scan中即可
scan.setFilter(filterList);
布隆过滤器
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
实际上,布隆过滤器广泛应用于网页黑名单系统、垃圾邮件过滤系统、爬虫网址判重系统等,Google 著名的分布式数据库 Bigtable 使用了布隆过滤器来查找不存在的行或列,以减少磁盘查找的 IO 次数,Google Chrome 浏览器使用了布隆过滤器加速安全浏览服务。
在很多 Key-Value 系统中也使用了布隆过滤器来加快查询过程,如 Hbase,Accumulo,Leveldb,一般而言,Value 保存在磁盘中,访问磁盘需要花费大量时间,然而使用布隆过滤器可以快速判断某个 Key 对应的 Value 是否存在,因此可以避免很多不必要的磁盘 IO 操作。
通过一个 Hash 函数将一个元素映射成一个位阵列(Bit Array)中的一个点。这样一来,我们只要看看这个点是不是 1 就知道可以集合中有没有它了。这就是布隆过滤器的基本思想。
运用场景
1、目前有 10 亿数量的自然数,乱序排列,需要对其排序。限制条件在 32 位机器上面完成,内存限制为 2G。如何完成?
2、如何快速在亿级黑名单中快速定位 URL 地址是否在黑名单中?(每条 URL 平均 64 字节)
3、需要进行用户登陆行为分析,来确定用户的活跃情况?
4、网络爬虫-如何判断 URL 是否被爬过?
5、快速定位用户属性(黑名单、白名单等)?
6、数据存储在磁盘中,如何避免大量的无效 IO?
7、判断一个元素在亿级数据中是否存在?
8、缓存穿透。
实现原理
假设我们有个集合 A,A 中有 n 个元素。利用k个哈希散列函数,将A中的每个元素映射到一个长度为 a 位的数组 B中的不同位置上,这些位置上的二进制数均设置为 1。如果待检查的元素,经过这 k个哈希散列函数的映射后,发现其 k 个位置上的二进制数全部为 1,这个元素很可能属于集合A,反之,一定不属于集合A。
比如我们有 3 个 URL {URL1,URL2,URL3},通过一个hash 函数把它们映射到一个长度为 16 的数组上,如下:
若当前哈希函数为 Hash1(),通过哈希运算映射到数组中,假设Hash1(URL1) = 4,Hash1(URL2) = 6,Hash1(URL3) = 6,如下:
因此,如果我们需要判断URL1是否在这个集合中,则通过Hash(urL1)计算出其下标,并得到其值若为 1 则说明存在。
由于 Hash 存在哈希冲突,如上面URL2,URL3都定位到一个位置上,假设 Hash 函数是良好的,如果我们的数组长度为 m 个点,那么如果我们想将冲突率降低到例如 1%, 这个散列表就只能容纳 m/100 个元素,显然空间利用率就变低了,也就是没法做到空间有效(space-efficient)。
解决方法也简单,就是使用多个 Hash 算法,如果它们有一个说元素不在集合中,那肯定就不在,如下:
Hash1(URL1) = 3,Hash2(URL1) = 5,Hash3(URL1) = 6
Hash1(URL2) = 5,Hash2(URL2) = 7,Hash3(URL2) = 13
Hash1(URL3) = 4,Hash2(URL3) = 7,Hash3(URL3) = 10
以上就是布隆过滤器做法,使用了k个哈希函数,每个字符串跟 k 个 bit 对应,从而降低了冲突的概率。
误判现象
上面的做法同样存在问题,因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他值置位了 1 ,那么程序还是会判断这个值存在。比如此时来一个不存在的 URL1000,经过哈希计算后,发现 bit 位为下:
Hash1(URL1000) = 7,Hash2(URL1000) = 8,Hash3(URL1000) = 14
但是上面这些 bit 位已经被URL1,URL2,URL3置为 1 了,此时程序就会判断 URL1000 值存在。
这就是布隆过滤器的误判现象,所以,布隆过滤器判断存在的不一定存在,但是,判断不存在的一定不存在。
布隆过滤器可精确的代表一个集合,可精确判断某一元素是否在此集合中,精确程度由用户的具体设计决定,达到 100% 的正确是不可能的。但是布隆过滤器的优势在于,利用很少的空间可以达到较高的精确率。
控制粒度
a)ROW
根据KeyValue中的行来过滤storefile
举例:假设有2个storefile文件sf1和sf2,
sf1包含kv1(r1 cf:q1 v),kv2(r2 cf:q1 v)
sf2包含kv3(r3 cf:q1 v),kv4(r4 cf:q1 v)
若是设置了CF属性中的bloomfilter为ROW,那么得(r1)时就会过滤sf2,get(r3)就会过滤sf1
b)ROWCOL
根据KeyValue中的行+限定符来过滤storefile
举例:假设有2个storefile文件sf1和sf2,
sf1包含kv1(r1 cf:q1 v),kv2(r2 cf:q1 v)
sf2包含kv3(r1 cf:q2 v),kv4(r2 cf:q2 v)
若是设置了CF属性中的布隆过滤器为ROW,不管获得(R1,Q1)仍是获得(R1,Q2),都会读取SF1 + SF2;而若是设置了CF属性中的布隆过滤器为 ROWCOL,那么GET(R1, q1)就会过滤sf2,get(r1,q2)就会过滤sf1
c)NO
默认的值,默认不开启布隆过滤器
实现:
在建立表时加入一个参数就能够了
try {
//使用HTableDescriptor类创建一个表对象
HTableDescriptor students = new HTableDescriptor("students");
//在创建表的时候,至少指定一个列簇
HColumnDescriptor info = new HColumnDescriptor("info");
//BloomType是一个枚举类此处设置粒度级别
info.setBloomFilterType(BloomType.ROW);
//将列簇添加到表中
students.addFamily(info);
//真正的执行,是由HMaster
//hAdmin
hAdmin.createTable(students);
System.out.println(Bytes.toString(students.getName()) + "表 创建成功。。。");
} catch (IOException e) {
e.printStackTrace();
}