目录
第 1 章:HBase 简介
1.1 HBase 定义
HBase
是一种分布式、可扩展、支持海量数据存储的
NoSQL
数据库。
1.2 HBase 数据模型
逻辑上,
HBase
的数据模型同关系型数据库很类似,数据存储在一张表中,有行有列。但从 HBase
的底层物理存储结构(
K-V
)来看,
HBase
更像是一个
multi-dimensional map。
1.2.1 HBase
逻辑结构
1.2.2 HBase
物理存储结构
1.2.3
数据模型
1
)
Name Space
命名空间,类似于关系型数据库的
DatabBase
概念,每个命名空间下有多个表。
HBase有两个自带的命名空间,分别是 hbase
和
default
,
hbase
中存放的是
HBase
内置的表,
default
表是用户默认使用的命名空间。
2
)
Region
类似于关系型数据库的表概念。不同的是,
HBase
定义表时只需要声明
列族
即可,不需要声明具体的列。这意味着,往 HBase
写入数据时,字段可以
动态
、
按需
指定。因此, 和关系型数据库相比,HBase
能够轻松应对字段变更的场景。
3
)
Row
HBase
表中的每行数据都由一个
RowKey
和多个
Column
(列)
组成,数据是按照RowKey 的
字典顺序存储
的,并且查询数据时只能根据
RowKey
进行检索,所以
RowKey的设计十分重要。
4
)
Column
HBase
中的每个列都由
Column Family(
列族
)
和
Column Qualifier
(列限定符)
进行限定,例如 info
:
name
,
info
:
age
。建表时,只需指明列族,而列限定符无需预先定义。
5
)
Time Stamp
用于标识数据的不同版本(version),每条数据写入时,如果不指定时间戳,系统会自动为其加上该字段,其值为写入 HBase
的时间。
6
)
Cell
由
{rowkey, column Family
:
column Qualifier, time Stamp}
唯一确定的单元。
cell
中的数据是没有类型的,全部是字节码形式存贮。
1.3 HBase 基本架构
架构角色:
1
)
Region Server
Region Server
为
Region
的管理者,其实现类为
HRegionServer
,主要作用如下:
①对于数据的操作:
get, put, delete
;
②对于
Region
的操作:
splitRegion
、
compactRegion
。
2
)
Master
Master
是所有
Region Server
的管理者,其实现类为
HMaster
,主要作用如下:
①对于表的操作:
create, delete, alter
②对于
RegionServer
的操作:分配
regions
到每个
RegionServer
,监控每个
RegionServer的状态,负载均衡和故障转移。
3
)
Zookeeper
HBase
通过
Zookeeper
来做
Master
的高可用、
RegionServer
的监控、元数据的入口以及集群配置的维护等工作。
4
)
HDFS
HDFS
为
HBase
提供最终的底层数据存储服务,同时为
HBase
提供高可用的支持。
第 2 章:HBase 快速入门
2.1 HBase 安装部署
2.1.1 Zookeeper
正常部署
首先保证
Zookeeper
集群的正常部署,并启动之:
[root@master zookeeper]# bin/zkServer.sh start[root@node1 zookeeper]# bin/zkServer.sh start[root@node2 zookeeper]# bin/zkServer.sh start
2.1.2 Hadoop
正常部署
Hadoop
集群的正常部署并启动:
[root@master hadoop]# sbin/start-all.sh
2.1.3 HBase
的解压
解压
Hbase
到指定目录:
[root@master package]# tar -zxvf hbase-1.4.6-bin.tar.gz -C /usr/local/soft/
2.1.4 HBase
的配置文件
修改
HBase
对应的配置文件。
1
)
hbase-env.sh
修改内容:
export JAVA_HOME=/usr/local/soft/jdk1.8.0_171export HBASE_MANAGES_ZK=false
2
)
hbase-site.xml
修改内容:
<configuration><property><name>hbase.rootdir</name><value>hdfs://master:9000/HBase</value></property><property><name>hbase.cluster.distributed</name><value>true</value></property><!-- 0.98 后的新变动,之前版本没有 .port, 默认端口为 60000 --><property><name>hbase.master.port</name><value>16000</value></property><property><name>hbase.zookeeper.quorum</name><value>master,node1,node2</value></property><property><name>hbase.zookeeper.property.dataDir</name><value>/usr/local/soft/hbase/zkData</value></property></configuration>
3
)
regionservers
:
masternode1node2
4
)软连接
hadoop
配置文件到
HBase
:
[root@master soft]# ln -s /usr/local/soft/hadoop-2.7.6/etc/hadoop/core-site.xml /usr/local/soft/hbase/conf/core-site.xml[root@master soft]# ln -s /usr/local/soft/hadoop-2.7.6/etc/hadoop/hdfs-site.xml /usr/local/soft/hbase/conf/hdfssite.xml
2.1.5 HBase
远程发送到其他集群
[root@master soft]# scp -r hbase node1:`pwd`[root@master soft]# scp -r hbase node2:`pwd`
2.1.6 HBase
服务的启动
1
.启动方式
[root@master hbase]# bin/hbase-daemon.sh start master[root@master hbase]# bin/hbase-daemon.sh start regionserver
提示:
如果集群之间的节点时间不同步,会导致
regionserver
无法启动,抛出 ClockOutOfSyncException 异常。
修复提示:
①百度,
linux
时间同步
②属性:
hbase.master.maxclockskew
设置更大的值
<property><name>hbase.master.maxclockskew</name><value>180000</value><description>Time difference of regionserver from master</description></property>
2
.启动方式
2
[root@master hbase]# bin/start-hbase.sh
对应的停止服务:
[root@master hbase]# bin/stop-hbase.sh
2.1.7
查看
HBase
页面
启动成功后,可以通过“host:port”的方式来访问
HBase
管理页面,例如:
http://master:16010
2.2 HBase Shell 操作
2.2.1
基本操作
1
.进入
HBase
客户端命令行
[root@master hbase]# bin/hbase shell
2
.查看帮助命令
hbase(main):001:0> help
3
.查看当前数据库中有哪些表
hbase(main):002:0> list
2.2.2
表的操作
1
.创建表
hbase(main):002:0> create 'student','info'
2
.插入数据到表
hbase(main):003:0> put 'student','1001','info:sex','male'hbase(main):004:0> put 'student','1001','info:age','18'hbase(main):005:0> put 'student','1002','info:name','Janna'hbase(main):006:0> put 'student','1002','info:sex','female'hbase(main):007:0> put 'student','1002','info:age','20'
3
.扫描查看表数据
hbase(main):008:0> scan 'student'hbase(main):009:0> scan 'student',{STARTROW => '1001', STOPROW => '1001'}hbase(main):010:0> scan 'student',{STARTROW => '1001'}
4
.查看表结构
hbase(main):011:0> describe ‘student’
5
.更新指定字段的数据
hbase(main):012:0> put 'student','1001','info:name','Nick'hbase(main):013:0> put 'student','1001','info:age','100'
6
.查看“指定行”或“指定列族
:
列”的数据
hbase(main):014:0> get 'student','1001'hbase(main):015:0> get 'student','1001','info:name'
7
.统计表数据行数
hbase(main):021:0> count 'student'
8
.删除数据
删除某
rowkey
的全部数据:
hbase(main):016:0> deleteall 'student','1001'
删除某
rowkey
的某一列数据:
hbase(main):017:0> delete 'student','1002','info:sex'
9
.清空表数据
hbase(main):018:0> truncate 'student'
提示:清空表的操作顺序为先
disable
,然后再
truncate
。
10
.删除表
首先需要先让该表为
disable
状态:
hbase(main):019:0> disable 'student'
然后才能
drop
这个表:
hbase(main):020:0> drop 'student'
提示:如果直接
drop
表,会报错:
ERROR: Table student is enabled. Disable it first.
11
.变更表信息
将
info
列族中的数据存放
3
个版本:
hbase(main):022:0> alter 'student',{NAME=>'info',VERSIONS=>3}hbase(main):022:0> get 'student','1001',{COLUMN=>'info:name',VERSIONS=>3}
第 3 章 HBase 进阶
3.1 架构原理
1
)
StoreFile
保存实际数据的物理文件,
StoreFile
以
HFile
的形式存储在
HDFS
上。每个
Store
会有一个或多个 StoreFile
(
HFile
),数据在每个
StoreFile
中都是有序的。
2
)
MemStore
写缓存,由于
HFile
中的数据要求是有序的,所以数据是先存储在
MemStore
中,排好序后,等到达刷写时机才会刷写到 HFile
,每次刷写都会形成一个新的
HFile
。
3
)
WAL
由于数据要经
MemStore
排序后才能刷写到
HFile
,但把数据保存在内存中会有很高的概率导致数据丢失,为了解决这个问题,数据会先写在一个叫做 Write-Ahead logfile
的文件中,然后再写入MemStore
中。所以在系统出现故障的时候,数据可以通过这个日志文件重建。
3.2 写流程
写流程:
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
。
3.3 MemStore Flush
MemStore
刷写时机:
1.
当某个
memstroe
的大小达到了
hbase.hregion.memstore.flush.size
(默认值
128M
)
,其所在
region
的所有
memstore
都会刷写
。
当
memstore
的大小达到了:
hbase.hregion.memstore.flush.size
(默认值
128M
)
hbase.hregion.memstore.block.multiplier
(默认值
4
) 时,会阻止继续
往该
memstore
写数据。
2.
当
region server
中
memstore
的总大小达到:
java_heapsize
*hbase.regionserver.global.memstore.size
(默认值
0.4
)
*hbase.regionserver.global.memstore.size.lower.limit
(默认值
0.95
)
region
会按照其所有
memstore
的大小顺序(由大到小)依次进行刷写。直到
region server 中所有
memstore
的总大小减小到上述值以下。
当
region server
中
memstore
的总大小达到
java_heapsize*hbase.regionserver.global.memstore.size
(默认值
0.4
)时,会阻止继续往所有的 memstore
写数据。
3.
到达自动刷写的时间,也会触发
memstore flush
。自动刷新的时间间隔由该属性进行配置 hbase.regionserver.optionalcacheflushinterval
(默认
1
小时)。
4.
当
WAL
文件的数量超过
hbase.regionserver.max.logs
,
region
会按照时间顺序依次进行刷写,直到 WAL
文件数量减小到
hbase.regionserver.max.log
以下(
该属性名已经废
弃,现无需手动设置,最大值为
32
)。
3.4 读流程
读流程
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
)分别在
Block Cache
(读缓存),
MemStore
和
Store File
(
HFile
)中查询目标数据,并将查到的所有数据进行合并。此处所有数据是指同一条数据的不同版本(time stamp)或者不同的类型(Put/Delete)。
5
) 将从文件中查询到的数据块(
Block
,
HFile
数据存储单元,默认大小为
64KB
)缓存到 Block Cache
。
6
)将合并后的最终结果返回给客户端。
3.5 StoreFile Compaction
由于
memstore
每次刷写都会生成一个新的
HFile
,且同一个字段的不同版本(timestamp) 和不同类型(Put/Delete)有可能会分布在不同的 HFile
中,因此查询时需要遍历所有的HFile。为了减少
HFile
的个数,以及清理掉过期和删除的数据,会进行
StoreFile Compaction
。Compaction 分为两种,分别是
Minor Compaction
和
Major Compaction
。
Minor Compaction 会将临近的若干个较小的
HFile
合并成一个较大的
HFile
,但
不会清理过期和
删除的数据
。Major Compaction 会将一个
Store
下的所有的
HFile
合并成一个大
HFile
,并且
会清
理掉过期和删除的数据
。
3.6 Region Split
默认情况下,每个
Table
起初只有一个
Region
,随着数据的不断写入,
Region
会自动进行拆分。刚拆分时,两个子 Region
都位于当前的
Region Server
,但处于负载均衡的考虑, HMaster
有可能会将某个
Region
转移给其他的
Region Server
。
Region Split
时机:
1.
当
1
个
region
中的某个
Store
下所有
StoreFile
的总大小超
hbase.hregion.max.filesize
,该 Region
就会进行拆分(0.94 版本之前)。
2.
当
1
个
region
中的某个
Store
下所有
StoreFile
的总大小超过
Min(R^2*"hbase.hregion.memstore.flush.size",hbase.hregion.max.filesize"),该
Region
就会进行拆分,其中 R
为当前
Region Server
中属于该
Table
的个数(0.94 版本之后)。
第 4 章 HBase API
4.1 环境准备
新建项目后在
pom.xml
中添加依赖:
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper --><dependency><groupId>org.apache.zookeeper</groupId><artifactId>zookeeper</artifactId><version>3.4.1</version></dependency><!--hbase--><dependency><groupId>org.apache.hbase</groupId><artifactId>hbase-client</artifactId><version>1.4.6</version></dependency>
4.2 HBaseAPI
4.2.1
获取
Configuration
对象
public static Configuration conf;static{// 使用 HBaseConfiguration 的单例方法实例化conf = HBaseConfiguration.create();conf.set("hbase.zookeeper.quorum", "192.168.152.100");conf.set("hbase.zookeeper.property.clientPort", "2181");}
4.2.2
判断表是否存在
public static boolean isTableExist(String tableName) throwsMasterNotRunningException,ZooKeeperConnectionException, IOException{// 在 HBase 中管理、访问表需要先创建 HBaseAdmin 对象//Connection connection = ConnectionFactory.createConnection(conf);//HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();HBaseAdmin admin = new HBaseAdmin(conf);return admin.tableExists(tableName);}
4.2.3
创建表
public static void createTable(String tableName, String...columnFamily) throwsMasterNotRunningException, ZooKeeperConnectionException,IOException{HBaseAdmin admin = new HBaseAdmin(conf);// 判断表是否存在if(isTableExist(tableName)){System.out.println(" 表 " + tableName + " 已存在 ");//System.exit(0);}else{// 创建表属性对象 , 表名需要转字节HTableDescriptor descriptor =newHTableDescriptor(TableName.valueOf(tableName));// 创建多个列族for(String cf : columnFamily){descriptor.addFamily(new HColumnDescriptor(cf));}// 根据对表的配置,创建表admin.createTable(descriptor);System.out.println(" 表 " + tableName + " 创建成功! ");}}
4.2.4
删除表
public static void dropTable(StringtableName) throwsMasterNotRunningException,ZooKeeperConnectionException, IOException{HBaseAdmin admin = new HBaseAdmin(conf);if(isTableExist(tableName)){admin.disableTable(tableName);admin.deleteTable(tableName);System.out.println(" 表 " + tableName + " 删除成功! ");}else{System.out.println(" 表 " + tableName + " 不存在! ");}}
4.2.5
向表中插入数据
public static void addRowData(String tableName, String rowKey,String columnFamily, Stringcolumn, String value) throws IOException{// 创建 HTable 对象HTable hTable = new HTable(conf, tableName);// 向表中插入数据Put put = new Put(Bytes.toBytes(rowKey));// 向 Put 对象中组装数据put.add(Bytes.toBytes(columnFamily),Bytes.toBytes(column),Bytes.toBytes(value));hTable.put(put);hTable.close();System.out.println(" 插入数据成功 ");}
4.2.6
删除多行数据
public static void deleteMultiRow(String tableName, String...rows)throws IOException{HTable hTable = new HTable(conf, tableName);List<Delete> deleteList = new ArrayList<Delete>();for(String row : rows){Delete delete = new Delete(Bytes.toBytes(row));deleteList.add(delete);}hTable.delete(deleteList);hTable.close();}
4.2.7
获取所有数据
public static void getAllRows(String tableName) throwsIOException{HTable hTable = new HTable(conf, tableName);// 得到用于扫描 region 的对象Scan scan = new Scan();// 使用 HTable 得到 resultcanner 实现类的对象ResultScanner resultScanner = hTable.getScanner(scan);for(Result result : resultScanner){Cell[] cells = result.rawCells();for(Cell cell : cells){// 得到 rowkeySystem.out.println("行 键:"+Bytes.toString(CellUtil.cloneRow(cell)));// 得到列族System.out.println("列 族" +Bytes.toString(CellUtil.cloneFamily(cell)));System.out.println("列:" +Bytes.toString(CellUtil.cloneQualifier(cell)));System.out.println("值:" +Bytes.toString(CellUtil.cloneValue(cell)));}}}
4.2.8
获取某一行数据
public static void getRow(String tableName, String rowKey)throwsIOException{HTable table = new HTable(conf, tableName);Get get = new Get(Bytes.toBytes(rowKey));//get.setMaxVersions(); 显示所有版本//get.setTimeStamp(); 显示指定时间戳的版本Result result = table.get(get);for(Cell cell : result.rawCells()){System.out.println("行 键:" +Bytes.toString(result.getRow()));System.out.println("列 族" +Bytes.toString(CellUtil.cloneFamily(cell)));System.out.println("列:" +Bytes.toString(CellUtil.cloneQualifier(cell)));System.out.println("值:" +Bytes.toString(CellUtil.cloneValue(cell)));System.out.println(" 时间戳 :" + cell.getTimestamp());}}
4.2.9
获取某一行指定“列族
:
列”的数据
public static void getRowQualifier(String tableName, StringrowKey,String family, Stringqualifier) throws IOException{HTable table = new HTable(conf, tableName);Get get = new Get(Bytes.toBytes(rowKey));get.addColumn(Bytes.toBytes(family),Bytes.toBytes(qualifier));Result result = table.get(get);for(Cell cell : result.rawCells()){System.out.println("行 键:" +Bytes.toString(result.getRow()));System.out.println("列 族" +Bytes.toString(CellUtil.cloneFamily(cell)));System.out.println("列:" +Bytes.toString(CellUtil.cloneQualifier(cell)));System.out.println("值:" +Bytes.toString(CellUtil.cloneValue(cell)));}}
第 5 章 Hbase 过滤器
5.1 Hbase 过滤器简介
HBase 的基本 API,包括增、删、改、查等。 增、删都是相对简单的操作,与传统的 RDBMS 相比,这里的查询操作略显苍白,只能根据特性的行键进行查询(Get)或者根据行键的范围来查询(Scan)。 HBase 不仅提供了这些简单的 查询,而且提供了更加高级的过滤器(Filter)来查询。
5.2 过滤器的两类参数
过滤器可以根据列族、列、版本等更多的条件来对数据进行过滤, 基于HBase 本身提供的三维有序(行键,列,版本有序),这些过滤器可以高效地完成查询过滤的任务,带有过滤器条件的 RPC 查询请求会把过滤器分发到各个RegionServer(这是一个服务端过滤器),这样也可以降低网络传输的压力。 使用过滤器至少需要两类参数: 一类是抽象的操作符,另一类是比较器
5.3 操作符
HBase 提供了枚举类型的变量来表示这些抽象的操作符:
LESS : 小于
LESS_OR_EQUAL : 小于等于
EQUAL : 等于
NOT_EQUAL:不等于
GREATER_OR_EQUAL : 大于等于
GREATER: 大于
NO_OP ; 不比较
5.4 比较器
比较器作为过滤器的核心组成之一,用于处理具体的比较逻辑,例如字节级的比较,字符串级的比较等。
(
1
)
RegexStringComparator :支持正则表达式的值比较
Scan scan = new Scan();RegexStringComparator comp = new RegexStringComparator( “文科 * ” );// 以 文科 开头的字符串SingleColumnValueFilter filter = newSingleColumnValueFilter(Bytes.toBytes("info"),Bytes.toBytes("clazz"), CompareOp.EQUAL, comp);scan.setFilter(filter);
(2)
SubStringComparator:用于监测一个子串是否存在于值中,并且不区分大小写。
Scan scan = new Scan();SubstringComparator comp = new SubstringComparator("1129");// 查找包含 1129 的字符串SingleColumnValueFilter filter = newSingleColumnValueFilter(Bytes.toBytes("info"),Bytes.toBytes("clazz"), CompareOp.EQUAL, comp);scan.setFilter(filter);
(3)
BinaryComparator:二进制比较器,用于按字典顺序比较 Byte 数据值。
Scan scan = new Scan();BinaryComparator comp = newBinaryComparator(Bytes.toBytes("xmei"));// ValueFilter filter = new ValueFilter(CompareOp.EQUAL, comp);scan.setFilter(filter);
(4)
BinaryPrefixComparator:前缀二进制比较器。与二进制比较器不同的是,只比较前缀是否相同。
Scan scan = new Scan();BinaryPrefixComparator comp = newBinaryPrefixComparator(Bytes.toBytes("yting")); //SingleColumnValueFilter filter = newSingleColumnValueFilter(Bytes.toBytes("family"),Bytes.toBytes("qualifier"), CompareOp.EQUAL, comp);scan.setFilter(filter);
5.5 过滤器
列值过滤器:效率较低,需要做全表扫描 SingleColumnValueFilter:用于测试值的情况(相等,不等,范围 、、、)
列簇过滤器: FamilyFilter:用于过滤列族(通常在 Scan 过程中通过设定某些列族来实现该功能,而不是直接使用该过滤器)。
列名过滤器: QualifierFilter:用于列名(Qualifier)过滤。
行键过滤器:效率较高,行键前缀过滤效率较高
RowFilter:行键过滤器,一般来讲,执行 Scan 使用 startRow/stopRow 方式比较好,而 RowFilter 过滤器也可以完成对某一行的过滤。
Bloom Filter 布隆过滤器
(1)Bloom Filter 简介
Bloom Filter(布隆过滤器)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。 在计算机科学中,我们常常会碰到时间换空间或者空间换时间的情况,即为了达到某一个方面的最优而牺牲另一个方面。
Bloom Filter 在时间空间这两个因素之外又引入了另一个因素:错误率。在使用 Bloom Filter 判断一个元素是否属于某个集合时,会有一定的错误率。也就是说,有可能把不属于这个集合的元素误认为属于这个集合(False Positive),但不会把属于这个集合的元素误认为不属于这个集合(False Negative)。在增加了错误率这个因素之后,Bloom Filter 通过允许少量的错误来节省大量的存储空间。它的用法其实是很容易理解的,我们拿个 HBase 中应用的例子来说下,我们已经知道 rowKey 存放在 HFile 中,那么为了从一系列的 HFile 中查询某个 rowkey,我们就可以通过 Bloom Filter 快速判断 rowkey 是否在这个 HFile 中,从而过滤掉大部分的 HFile,减少需要扫描的 Block。
(2)
Bloom Filter
工作原理
BloomFilter 对于 HBase 的随机读性能至关重要,对于 get 操作以及部分scan 操作可以剔除掉不会用到的 HFile 文件,减少实际 IO 次数,提高随机读性能。在此简单地介绍一下 Bloom Filter 的工作原理,Bloom Filter 使用位数组来实现过滤,初始状态下位数组每一位都为 0,如下图所示:
假如此时有一个集合 S = {x1, x2, … xn},Bloom Filter 使用 k 个独立的hash 函数,分别将集合中的每一个元素映射到{1,…,m}的范围。对于任何一个元素,被映射到的数字作为对应的位数组的索引,该位会被置为 1。比如元素 x1 被 hash 函数映射到数字 8,那么位数组的第 8 位就会被置为 1。下图中集合 S只有两个元素 x 和 y,分别被 3 个 hash 函数进行映射,映射到的位置分别为(
0,3,6)和(4,7,10),对应的位会被置为 1:
现在假如要判断另一个元素是否是在此集合中,只需要被这 3 个 hash 函数 进行映射,查看对应的位置是否有 0 存在,如果有的话,表示此元素肯定不存在 于这个集合,否则有可能存在。下图所示就表示 z 肯定不在集合{x,y}中:
从上面的内容我们可以得知,Bloom Filter 有两个很重要的参数
哈希函数个数
位数组的大小
(3)
Bloom Filter 在 HBase 中的应用
HFile 中和 Bloom Filter 相关的 Block, Scanned Block Section(扫描 HFile 时被读取):Bloom Block
Load-on-open-section(regionServer 启动时加载到内存):BloomFilter Meta Block、Bloom Index Block
Bloom Block:Bloom 数据块,存储 Bloom 的位数组
Bloom Index Block:Bloom 数据块的索引
BloomFilter Meta Block:从 HFile 角度看 bloom 数据块的一些元数据信息,大小个数等等。 HBase 中每个 HFile 都有对应的位数组,KeyValue 在写入 HFile时会先经过几个 hash 函数的映射,映射后将对应的数组位改为 1,get 请求进来之后再进行 hash 映射,如果在对应数组位上存在 0,说明该 get 请求查询的数据不在该 HFile 中。
HFile 中的 Bloom Block 中存储的就是上面说得位数组,当 HFile 很大时,Data Block 就会很多,同时 KeyValue 也会很多,需要映射入位数组的 rowKey也会很多,所以为了保证准确率,位数组就会相应越大,那 Bloom Block 也会越大,为了解决这个问题就出现了 Bloom Index Block,一个 HFile 中有多个 Bloom Block(位数组),根据 rowKey 拆分,一部分连续的 Key 使用一个位数组。这样查询 rowKey 就要先经过 Bloom Index Block(在内存中)定位到 Bloom Block,再把Bloom Block 加载到内存,进行过滤。
5.6 示列代码
写代码之前需要将
hbase
中的配置文件
hbase-site.xml
文件放在
idea
的
resource
文件夹
下面
代码实现
package shujia;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.TableName;
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;
public class Demo4Filter {
Connection conn;
Admin admin;
TableName studentTN;
Table student;
public void printRSWithFilter(Filter filter) throws
IOException {
Scan scan = new Scan();
scan.setFilter(filter);
ResultScanner scanner = student.getScanner(scan);
for (Result rs : scanner) {
String id = Bytes.toString(rs.getRow());
String name =
Bytes.toString(rs.getValue("info".getBytes(),
"name".getBytes()));
String age =
Bytes.toString(rs.getValue("info".getBytes(),
"age".getBytes()));
String gender =
Bytes.toString(rs.getValue("info".getBytes(),
"gender".getBytes()));
String clazz =
Bytes.toString(rs.getValue("info".getBytes(),
"clazz".getBytes()));
System.out.println(id + "," + name + "," + age + "," +
gender + "," + clazz);
}
}
@Before
public void createConn() throws IOException {
// 1、创建一个配置文件
Configuration conf = HBaseConfiguration.create();
// 配置 ZK 的地址,通过 ZK 可以找到 HBase
conf.set("hbase.zookeeper.quorum",
"master:2181,node1:2181,node2:2181");
// 2、创建连接
conn = ConnectionFactory.createConnection(conf);
// 3、创建 Admin 对象
admin = conn.getAdmin();
studentTN = TableName.valueOf("student");
student = conn.getTable(studentTN);
}
@Test
/**
* age > 23 的学生
* ValueFilter:作用在每一个 cell 上,符合要求的 cell 不会被过滤
* 结果不符合正常的思维逻辑
*/
public void ValueFilter1() throws IOException {
// 创建二进制比较器
BinaryComparator binaryComparator = new
BinaryComparator("23".getBytes());
// 列值过滤器
ValueFilter valueFilter = new
ValueFilter(CompareFilter.CompareOp.GREATER, binaryComparator);
printRSWithFilter(valueFilter);
}
@Test
/**
* SingleColumnValueFilter 单列值过滤器
* 可以指定一个列进行过滤
* 该过滤器会将符合过滤条件的列对应的 cell 所在的整行数据进行返回
* 如果某条数据的列不符合条件,则会将整条数据进行过滤
* 如果数据中不存在指定的列,则默认会直接返回
*
* age > 23 的学生
*/
public void SingleColumnValueFilter1() throws IOException {
SingleColumnValueFilter singleColumnValueFilter = new
SingleColumnValueFilter(
"info".getBytes(),
"age".getBytes(),
CompareFilter.CompareOp.GREATER,
"23".getBytes()
);
printRSWithFilter(singleColumnValueFilter);
}
@Test
/**
* 过滤文科班的学生
* SingleColumnValueExcludeFilter 单列值排除过滤器
* 同单列值过滤器最大的区别在于最后的返回结果包不包含比较的列
*/
public void SingleColumnValueExcludeFilter2() throws
IOException {
BinaryPrefixComparator binaryPrefixComparator = new
BinaryPrefixComparator("文科".getBytes());
SingleColumnValueExcludeFilter
singleColumnValueExcludeFilter = new
SingleColumnValueExcludeFilter(
"info".getBytes(),
"clazz".getBytes(),
CompareFilter.CompareOp.EQUAL,
binaryPrefixComparator
);
printRSWithFilter(singleColumnValueExcludeFilter);
}
@Test
/**
* 过滤出 rowkey(id)以 150010088 开头的学生
*/
public void RowFilter1() throws IOException {
BinaryPrefixComparator binaryPrefixComparator = new
BinaryPrefixComparator("150010088".getBytes());
RowFilter rowFilter = new
RowFilter(CompareFilter.CompareOp.EQUAL,
binaryPrefixComparator);
printRSWithFilter(rowFilter);
}
@Test
/**
* 过滤出 rowkey(id)以 150010088 开头的学生
* 使用 PrefixFilter:rowkey 前缀过滤器
* 相当于 BinaryPrefixComparator+RowFilter
*/
public void PrefixFilter1() throws IOException {
PrefixFilter prefixFilter = new
PrefixFilter("150010088".getBytes());
printRSWithFilter(prefixFilter);
}
@Test
/**
* 通过正则表达式: [A-Za-z0-9]{1}f[0-9]+ 过滤出符合条件的列簇下的
所有 cell
*/
public void RegexFamilyFilter() throws IOException {专注于大数据!
RegexStringComparator regexStringComparator = new
RegexStringComparator("[A-Za-z0-9]{1}f[0-9]+");
FamilyFilter familyFilter = new
FamilyFilter(CompareFilter.CompareOp.EQUAL,
regexStringComparator);
Scan scan = new Scan();
scan.setFilter(familyFilter);
ResultScanner scanner = student.getScanner(scan);
for (Result rs : scanner) {
for (Cell cell : rs.listCells()) {
String cf =
Bytes.toString(CellUtil.cloneFamily(cell));
String q =
Bytes.toString(CellUtil.cloneQualifier(cell));
String value =
Bytes.toString(CellUtil.cloneValue(cell));
System.out.println(cf + ":" + q + " " + value);
}
}
}
@Test
/**
* 过滤出列名包含 q 的所有的 cell
*
*/
public void SubStringQualifierFilter() throws IOException {
SubstringComparator comparator = new
SubstringComparator("q");
QualifierFilter qualifierFilter = new
QualifierFilter(CompareFilter.CompareOp.EQUAL, comparator);
Scan scan = new Scan();
scan.setFilter(qualifierFilter);
ResultScanner scanner = student.getScanner(scan);
for (Result rs : scanner) {
for (Cell cell : rs.listCells()) {
String cf =
Bytes.toString(CellUtil.cloneFamily(cell));
String q =
Bytes.toString(CellUtil.cloneQualifier(cell));
String value =
Bytes.toString(CellUtil.cloneValue(cell));
System.out.println(cf + ":" + q + " " + value);
}
}
}
@Test
/**
* 分页过滤器:PageFilter
* 获取第四页的数据,每页 10 条
*
* 实际上需要遍历该页前面所有的数据,性能非常低
*/
public void PageFilter1() throws IOException {
int page = 4;
int pageSize = 10;
// 首先先获取第 4 页的第一条数据的 rk
int page_first = (page - 1) * pageSize + 1;
PageFilter pageFilter1 = new PageFilter(page_first);
Scan scan = new Scan();
scan.setFilter(pageFilter1);
//
scan.setLimit(40); // PageFilter 就相当于 setLimit
String rowkey = null;
ResultScanner scanner = student.getScanner(scan);
for (Result rs : scanner) {
rowkey = Bytes.toString(rs.getRow());
}
Scan scan1 = new Scan();
scan1.withStartRow(rowkey.getBytes());
PageFilter pageFilter2 = new PageFilter(pageSize);
scan1.setFilter(pageFilter2);
ResultScanner scanner2 = student.getScanner(scan1);
for (Result rs : scanner2) {
String id = Bytes.toString(rs.getRow());
String name =
Bytes.toString(rs.getValue("info".getBytes(),
"name".getBytes()));
String age =
Bytes.toString(rs.getValue("info".getBytes(),
"age".getBytes()));
String gender =
Bytes.toString(rs.getValue("info".getBytes(),
"gender".getBytes()));
String clazz =
Bytes.toString(rs.getValue("info".getBytes(),
"clazz".getBytes()));
System.out.println(id + "," + name + "," + age + "," +
gender + "," + clazz);
}
}
@Test
/**
* 通过合理地设计 Rowkey 来实现分页的功能
* 获取第五页的数据 每页还是 10 条
*
*/
public void PageWithRowkey() throws IOException {
int page = 5;
int pageSize = 10;
int baseId = 1500100000;
int current_page_first_rk = baseId + (page - 1) * pageSize
+ 1;
Scan scan = new Scan();
scan.withStartRow((current_page_first_rk +
"").getBytes());
scan.setLimit(pageSize);
ResultScanner scanner = student.getScanner(scan);
for (Result rs : scanner) {
String id = Bytes.toString(rs.getRow());
String name =
Bytes.toString(rs.getValue("info".getBytes(),
"name".getBytes()));
String age =
Bytes.toString(rs.getValue("info".getBytes(),
"age".getBytes()));
String gender =
Bytes.toString(rs.getValue("info".getBytes(),
"gender".getBytes()));
String clazz =
Bytes.toString(rs.getValue("info".getBytes(),
"clazz".getBytes()));
System.out.println(id + "," + name + "," + age + "," +
gender + "," + clazz);
}
}
@Test
/**
* 过滤 gender 为男,age>23,理科班的学生
* 过条件过滤需要使用 FilterList
*/
public void MultipleFilter() throws IOException {
SingleColumnValueFilter filter1 = new
SingleColumnValueFilter(
"info".getBytes(),
"gender".getBytes(),
CompareFilter.CompareOp.EQUAL,
"男".getBytes()
);
SingleColumnValueFilter filter2 = new
SingleColumnValueFilter(
"info".getBytes(),
"age".getBytes(),
CompareFilter.CompareOp.GREATER,
"23".getBytes()
);
SingleColumnValueFilter filter3 = new
SingleColumnValueFilter(
"info".getBytes(),
"clazz".getBytes(),
CompareFilter.CompareOp.EQUAL,
new BinaryPrefixComparator("理科".getBytes())
);
FilterList filterList = new FilterList();
filterList.addFilter(filter1);
filterList.addFilter(filter2);
filterList.addFilter(filter3);
printRSWithFilter(filterList);
}
@After
public void close() throws IOException {
admin.close();
conn.close();
}
}
第 6 章 Phoenix
6.1 Phoenix 简介
Hbase
适合存储大量的对关系运算要求低的
NOSQL
数据,受
Hbase
设计上的限制不能直接使用原生的API
执行在关系数据库中普遍使用的条件判断和聚合等操作。
Hbase
很优秀,一些团队寻求在Hbase
之上提供一种更面向普通开发人员的操作方式,
Apache Phoenix
即是。
Phoenix 基于
Hbase
给面向业务的开发人员提供了以标准
SQL
的方式对
Hbase
进行查询操作,并支持标准 SQL
中大部分特性
:
条件运算
,
分组,分页,等高级查询语法。
6.2 Phoenix 搭建
Phoenix 4.15 HBase 1.4.6 hadoop 2.7.6
1
、关闭
hbase
集群,在
master
中执行
stop-hbase.sh
2
、上传解压配置环境变量
tar -xvf apache-phoenix-4.15.0-HBase-1.4-bin.tar.gz -C /usr/local/soft/mv apache-phoenix-4.15.0-HBase-1.4-bin phoenix-4.15.0
3
、将
phoenix-4.15.0-HBase-1.4-server.jar
复制到所有节点的
hbase lib
目录下
scp /usr/local/soft/phoenix-4.15.0/phoenix-4.15.0-HBase-1.4-server.jar master:/usr/local/soft/hbase-1.4.6/lib/scp /usr/local/soft/phoenix-4.15.0/phoenix-4.15.0-HBase-1.4-server.jar node1:/usr/local/soft/hbase-1.4.6/lib/scp /usr/local/soft/phoenix-4.15.0/phoenix-4.15.0-HBase-1.4-server.jar node2:/usr/local/soft/hbase-1.4.6/lib/
4
、启动
hbase
, 在
master
中执行
start-hbase.sh
5
、配置环境变量
vim /etc/profileexport PHOENIX_HOME=/usr/local/soft/hbase-1.4.6
export PHOENIX_CLASSPATH=$PHOENIX_HOME
export PATH=$PATH:$PHOENIX_HOME/bin
6.3 Phoenix 使用
1.
连接
sqlline
sqlline.py master,node1,node2# 出现163/163 (100%) DoneDonesqlline version 1.5.00: jdbc:phoenix:master,node1,node2>
2
、常用命令
# 1
、创建表
CREATE TABLE IF NOT EXISTS STUDENT (id VARCHAR NOT NULL PRIMARY KEY,name VARCHAR,age BIGINT,gender VARCHAR ,clazz VARCHAR);
# 2
、显示所有表
!table
# 3
、插入数据
upsert into STUDENT values('1500100004',' 葛德曜 ',24,' 男 ',' 理科三班 ');upsert into STUDENT values('1500100005',' 宣谷芹 ',24,' 男 ',' 理科六班 ');upsert into STUDENT values('1500100006',' 羿彦昌 ',24,' 女 ',' 理科三班 ');
# 4
、查询数据
,
支持大部分
sql
语法,
select * from STUDENT ;select * from STUDENT where age=24;select gender ,count(*) from STUDENT group by gender;select * from student order by gender;
# 5
、删除数据
delete from STUDENT where id='1500100004';
# 6
、删除表
drop table STUDENT;
# 7
、退出命令行
!quit
更多语法参照官网
https://phoenix.apache.org/language/index.html#upsert_select
3
、
phoenix
表映射
默认情况下,直接在
hbase
中创建的表,通过
phoenix
是查看不到的
如果需要在
phoenix
中操作直接在
hbase
中创建的表,则需要在
phoenix
中进行表的映射。映射方式有两种:视图映射和表映射
视图映射
Phoenix
创建的视图是只读的,所以只能用来做查询,无法通过视图对源数据进行修改等操作
# hbase shell 进入 hbase 命令行hbase shell# 创建 hbase 表create 'test','name','company'# 插入数据put 'test','001','name:firstname','zhangsan1'put 'test','001','name:lastname','zhangsan2'put 'test','001','company:name',' 数加 'put 'test','001','company:address',' 合肥 '# 在 phoenix 创建视图, primary key 对应到 hbase 中的 rowkeycreate view "test"(empid varchar primary key,"name"."firstname" varchar,"name"."lastname" varchar,"company"."name" varchar,"company"."address" varchar);CREATE view "student" (id VARCHAR NOT NULL PRIMARY KEY,"info"."name" VARCHAR,"info"."age" VARCHAR,"info"."gender" VARCHAR ,"info"."clazz" VARCHAR) column_encoded_bytes=0;# 在 phoenix 查询数据,表名通过双引号引起来select * from "test";# 删除视图drop view "test";
表映射
使用
Apache Phoenix
创建对
HBase
的表映射,有两类:
1
)当
HBase
中已经存在表时,可以以类似创建视图的方式创建关联表,只需要将
create view 改为
create table
即可。
2
)当
HBase
中不存在表时,可以直接使用
create table
指令创建需要的表,并且在创建指令中可以根据需要对 HBase
表结构进行显示的说明。
第
1
)种情况下,如在之前的基础上已经存在了
test
表,则表映射的语句如下:
create table "test" (empid varchar primary key,"name"."firstname" varchar,"name"."lastname"varchar,"company"."name" varchar,"company"."address" varchar)column_encoded_bytes=0;upsert into "test" values('1','2','3','4','5');CREATE table "student" (id VARCHAR NOT NULL PRIMARY KEY,"info"."name" VARCHAR,"info"."age" VARCHAR,"info"."gender" VARCHAR ,"info"."clazz" VARCHAR) column_encoded_bytes=0;upsert into "student" values('1500110004',' 葛德曜 ','24','n ü',' 理科三班 ');
使用
create table
创建的关联表,如果对表进行了修改,源数据也会改变,同时如果关联表被删除,源表也会被删除。但是视图就不会,如果删除视图,源数据不会发生改变。
6.4 Phoenix 二级索引
对于
Hbase
,如果想精确定位到某行记录,唯一的办法就是通过
rowkey
查询。如果不通过 rowkey
查找数据,就必须逐行比较每一行的值,对于较大的表,全表扫描的代价是不可接受的。
1.
开启索引支持
#
关闭
hbase
集群
stop-hbase.sh
#
在
/usr/local/soft/hbase-1.4.6/conf/hbase-site.xml
中增加如下配置
<property><name>hbase.regionserver.wal.codec</name><value>org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec</value></property><property><name>hbase.rpc.timeout</name><value>60000000</value></property><property><name>hbase.client.scanner.timeout.period</name><value>60000000</value></property><property><name>phoenix.query.timeoutMs</name><value>60000000</value></property># 同步到所有节点scp hbase-site.xml node1:`pwd`scp hbase-site.xml node2:`pwd`# 修改 phoenix 目录下的 bin 目录中的 hbase-site.xml<property><name>hbase.rpc.timeout</name><value>60000000</value></property><property><name>hbase.client.scanner.timeout.period</name><value>60000000</value></property><property><name>phoenix.query.timeoutMs</name><value>60000000</value></property>
#
启动
hbase
start-hbase.sh
#
重新进入
phoenix
客户端
sqlline.sql master,node1,node2
2
、创建索引
(
1
)全局索引
全局索引适合读多写少的场景。如果使用全局索引,读数据基本不损耗性能,所有的性能损耗都来源于写数据。数据表的添加、删除和修改都会更新相关的索引表(数据删除了,索引表中的数据也会删除;数据增加了,索引表的数据也会增加)
注意
:
对于全局索引在默认情况下,在查询语句中检索的列如果不在索引表中,
Phoenix不会使用索引表将,除非使用 hint
。
# 创建 DIANXIN.sqlCREATE TABLE IF NOT EXISTS DIANXIN (mdn VARCHAR ,start_date VARCHAR ,end_date VARCHAR ,county VARCHAR,x DOUBLE ,y DOUBLE,bsid VARCHAR,grid_id VARCHAR,biz_type VARCHAR,event_type VARCHAR ,data_source VARCHAR ,CONSTRAINT PK PRIMARY KEY (mdn,start_date)) column_encoded_bytes=0;# 上传数据 DIANXIN.csv# 导入数据psql.py master,node1,node2 DIANXIN.sql DIANXIN.csv# 创建全局索引CREATE INDEX DIANXIN_INDEX ON DIANXIN ( end_date );# 查询数据 ( 索引未生效 )select * from DIANXIN where end_date = '20180503154014';# 强制使用索引 (索引生效) hintselect /*+ INDEX(DIANXIN DIANXIN_INDEX) */ * from DIANXIN where end_date = '20180503154014';select /*+ INDEX(DIANXIN DIANXIN_INDEX) */ * from DIANXIN where end_date = '20180503154014' and start_date = '20180503154614';# 取索引列,(索引生效)select end_date from DIANXIN where end_date = '20180503154014';# 创建多列索引CREATE INDEX DIANXIN_INDEX1 ON DIANXIN ( end_date,COUNTY );# 多条件查询 (索引生效)select end_date,MDN,COUNTY from DIANXIN where end_date = '20180503154014' and COUNTY = '8340104';# 查询所有列 ( 索引未生效 )select * from DIANXIN where end_date = '20180503154014' and COUNTY = '8340104';# 查询所有列 (索引生效)select /*+ INDEX(DIANXIN DIANXIN_INDEX1) */ * from DIANXIN where end_date = '20180503154014' and COUNTY = '8340104';# 单条件 ( 索引未生效 )select end_date from DIANXIN where COUNTY = '8340103';# 单条件 ( 索引生效 ) end_date 在前select COUNTY from DIANXIN where end_date = '20180503154014';# 删除索引drop index DIANXIN_INDEX on DIANXIN;
(
2
)本地索引
本地索引适合写多读少的场景,或者存储空间有限的场景。和全局索引一样,
Phoenix也会在查询的时候自动选择是否使用本地索引。本地索引因为索引数据和原数据存储在同一台机器上,避免网络数据传输的开销,所以更适合写多的场景。由于无法提前确定数据在哪个 Region
上,所以在读数据的时候,需要检查每个
Region
上的数据从而带来一些性能损耗。
注意
:
对于本地索引,查询中无论是否指定
hint
或者是查询的列是否都在索引表中,都会使用索引表。
#
创建本地索引
CREATE LOCAL INDEX DIANXIN_LOCAL_IDEX ON DIANXIN(grid_id);
#
索引生效
select grid_id from dianxin where grid_id='117285031820040';
#
索引生效
select * from dianxin where grid_id='117285031820040';
(
3
)覆盖索引
覆盖索引是把原数据存储在索引数据表中,这样在查询时不需要再去
HBase
的原表获取数据就直接返回查询结果。
注意:查询是
select
的列和
where
的列都需要在索引中出现。
# 创建覆盖索引CREATE INDEX DIANXIN_INDEX_COVER ON DIANXIN ( x,y ) INCLUDE ( county );# 查询所有列 ( 索引未生效 )select * from DIANXIN where x=117.288 and y =31.822;# 强制使用索引 ( 索引生效 )select /*+ INDEX(DIANXIN DIANXIN_INDEX_COVER) */ * from DIANXIN where x=117.288 and y =31.822;# 查询索引中的列 ( 索引生效 ) mdn 是 DIANXIN 表的 RowKey 中的一部分select x,y,county from DIANXIN where x=117.288 and y =31.822;select mdn,x,y,county from DIANXIN where x=117.288 and y =31.822;# 查询条件必须放在索引中 select 中的列可以放在 INCLUDE (将数据保存在索引中)select /*+ INDEX(DIANXIN DIANXIN_INDEX_COVER) */ x,y,count(*) from DIANXIN group by x,y;
6.5 Phoenix JDBC
#
导入依赖
<dependency><groupId>org.apache.phoenix</groupId><artifactId>phoenix-core</artifactId><version>4.15.0-HBase-1.4</version></dependency>
Connection conn = DriverManager.getConnection("jdbc:phoenix:master,node1,node2:2181");
PreparedStatement ps = conn.prepareStatement("select /*+ INDEX(DIANXIN DIANXIN_INDEX) */ * from DIANXIN where end_date=?");
ps.setString(1, "20180503212649");
ResultSet rs = ps.executeQuery();
while (rs.next()) {
String mdn = rs.getString("mdn");
String start_date = rs.getString("start_date");
String end_date = rs.getString("end_date");
String x = rs.getString("x");
String y = rs.getString("y");
String county = rs.getString("county");
System.out.println(mdn + "\t" + start_date + "\t" + end_date + "\t" + x + "\t" + y + "\t" + county);
}
ps.close();
conn.close();
6.6 Phoenix 调优
学习链接:
https://blog.csdn.net/jy02268879/article/details/81396026
1
、建立索引超时,查询超时
修改配置文件,
hbase-site.xml
两个位置
/usr/local/soft/phoenix-4.15.0/bin
/usr/local/soft/hbase-1.4.6/conf/
所有节点
增加配置
<property><name>hbase.rpc.timeout</name><value>60000000</value></property><property><name>hbase.client.scanner.timeout.period</name><value>60000000</value></property><property><name>phoenix.query.timeoutMs</name><value>60000000</value></property>
需要重启
hbase
2
、预分区
CREATE TABLE IF NOT EXISTS STUDENT2 (id VARCHAR NOT NULL PRIMARY KEY,name VARCHAR,age BIGINT,gender VARCHAR ,clazz VARCHAR)split on('15001006|','15001007|','15001008|') ;
3
、在创建表的时候指定
salting
。会再
rowkey
前面加上一个随机的前缀,
优点:不需要知道
rowkey
的分步情况
缺点:不能再
hbase
中对数据进行查询和修改
CREATE TABLE IF NOT EXISTS STUDENT3 (id VARCHAR NOT NULL PRIMARY KEY,name VARCHAR,age BIGINT,gender VARCHAR ,clazz VARCHAR)salt_buckets=6;upsert into STUDENT3 values('1500100004',' 葛德曜 ',24,' 男 ',' 理科三班 ');upsert into STUDENT3 values('1500100005',' 宣谷芹 ',24,' 男 ',' 理科六班 ');upsert into STUDENT3 values('1500100006',' 羿彦昌 ',24,' 女 ',' 理科三班 ');
4
、二级索引
建立行键与列值的映射关系
全局索引:读多写少, 会单独建立索引表
本地索引:读少写多, 索引数据和原数据保存在同一台机器上
全局索引,本地索引不同点 和 比较
直白话:全局索引是表,适合重读轻写的场景 , 本地索引是列族,适合重写轻读的场景
1.
索引数据
global index
单独把索引数据存到一张表里,保证了原始数据的安全,侵入性小
local index
把数据写到原始数据里面,侵入性强,原表的数据量
=
原始数据
+
索引数据,使原始数据更大
2.
性能方面
global index
要多写出来一份数据,写的压力就大一点,但读的速度就非常快
local index
只用写一份索引数据,节省不少空间,但多了一步通过
rowkey
查找数据,写的速度非常快,读的速度就没有直接取自己的列族数据快。
第 7 章 Rowkey 的设计
HBase
是三维有序存储的,通过
rowkey
(行键),
column key
(column family 和
qualifier
)和 TimeStamp
(时间戳)这个三个维度可以对
HBase
中的数据进行快速定位。 HBase 中
rowkey
可以唯一标识一行记录,在
HBase
查询的时候,有三种方式:
1.
通过
get
方式,指定
rowkey
获取唯一一条记录
2.
通过
scan
方式,设置
startRow
和
stopRow
参数进行范围匹配
3.
全表扫描,即直接扫描整张表中所有行记录
rowkey
长度原则
rowkey
是一个二进制码流,可以是任意字符串,最大长度
64kb
,实际应用中一般为10-100bytes,以
byte[]
形式保存,一般设计成定长。
建议越短越好,不要超过
16
个字节,原因如下:
数据的持久化文件
HFile
中是按照
KeyValue
存储的,如果
rowkey
过长,比如超过
100
字节, 1000w 行数据,光
rowkey
就要占用
100*1000w=10
亿个字节,将近
1G
数据,这样会极大影响
HFile 的存储效率;
MemStore
将缓存部分数据到内存,如果
rowkey
字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。
rowkey
散列原则
如果
rowkey
按照时间戳的方式递增,不要将时间放在二进制码的前面,建议将
rowkey
的高位作为散列字段,由程序随机生成,低位放时间字段,这样将提高数据均衡分布在每个RegionServer
,
以实现负载均衡的几率。如果没有散列字段,首字段直接是时间信息,所有的数据都会集中在一个RegionServer 上,这样在数据检索的时候负载会集中在个别的
RegionServer
上,造成热点问题,会降低查询效率。
rowkey
唯一原则
必须在设计上保证其唯一性,
rowkey
是按照字典顺序排序存储的,因此,设计
rowkey
的时候, 要充分利用这个排序的特点,将经常读取的数据存储到一块,将最近可能会被访问的数据放到块。
热点问题
HBase
中的行是按照
rowkey
的字典顺序排序的,这种设计优化了
scan
操作,可以将相关的行以及会被一起读取的行存取在临近位置,便于 scan
。然而糟糕的
rowkey
设计是热点的源头。 热点 发生在大量的 client
直接访问集群的一个或极少数个节点(访问可能是读,写或者其他操作)。大量 访问会使热点 region
所在的单个机器超出自身承受能力,引起性能下降甚至
region
不可用,这也会 影响同一个 RegionServer
上的其他
region
,由于主机无法服务其他
region
的请求。 设计良好的数据访问模式以使集群被充分,均衡的利用。
为了避免写热点,设计
rowkey
使得不同行在同一个
region
,但是在更多数据情况下,数据应该被写入集群的多个 region
,而不是一个。
下面是一些常见的避免热点的方法以及它们的优缺点:
加盐
这里所说的加盐不是密码学中的加盐,而是在
rowkey
的前面增加随机数,具体就是给
rowkey分配一个随机前缀以使得它和之前的 rowkey
的开头不同。分配的前缀种类数量应该和你想使用数据 分散到不同的 region
的数量一致。加盐之后的
rowkey
就会根据随机生成的前缀分散到各个
region 上,以避免热点。
哈希
哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的 rowkey
,可以使用
get
操作准确获取某一个行数据
反转
第三种防止热点的方法时反转固定长度或者数字格式的
rowkey
。这样可以使得
rowkey
中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机 rowkey
,但是牺牲了
rowkey
的 有序性。
反转
rowkey
的例子以手机号为
rowkey
,可以将手机号反转后的字符串作为
rowkey
,这样的就避免了以手机号那样比较固定开头导致热点问题
时间戳反转
一个常见的数据处理问题是快速获取数据的最近版本,使用反转的时间戳作为
rowkey
的一部分对这个问题十分有用,可以用 Long.Max_Value - timestamp
追加到
key
的末尾,例如[key][reverse_timestamp] , [key] 的最新值可以通过
scan [key]
获得
[key]
的第一条记录,因为HBase 中
rowkey
是有序的,第一条记录是最后录入的数据。比如需要保存一个用户的操作记录,按照操作时间倒序排序,在设计 rowkey
的时候,可以这样设计[userId 反转
][Long.Max_Value - timestamp]
,在查询用户的所有操作记录数据的时候,直接指定反转后的 userId
,
startRow
是
[userId
反转
][000000000000],stopRow
是
[userId
反转][Long.Max_Value - timestamp] 如果需要查询某段时间的操作记录,startRow
是
[user
反转
][Long.Max_Value -
起始时间
]
,stopRow 是
[userId
反转
][Long.Max_Value -
结束时间
]
其他一些建议
尽量减少
rowkey
和列的大小,当具体的值在系统间传输时,它的
rowkey
,列簇、列名,时间戳也会一起传输。如果你的 rowkey
、列簇名、列名很大,甚至可以和具体的值相比较,那么将会造成大量的冗余,不利于数据的储存与传输列族尽可能越短越好,最好是一个字符列名也尽可能越短越好,冗长的列名虽然可读性好,但是更短的列名存储在 HBase
中会更好
第 8 章 其他
8.1 version 和 ttl
hbase
可以保存多个版本的数据
create 'User',{NAME => 'info',VERSIONS => 5}put 'User','row1','info:age','21'put 'User','row1','info:age','22'put 'User','row1','info:age','23'put 'User','row1','info:age','24'put 'User','row1','info:age','25'put 'User','row1','info:age','26'
保存最新
5
个版本的数据
指定查询多个版本的数据
scan 'User' ,{VERSIONS => 5}get 'User','row1', {COLUMN => 'info:age', VERSIONS => 5}hbsae 提供了 ttl 过期时间create 'User1',{NAME => 'info',TTL => 5}put 'User1','row1','info:age','21'
5
秒之后自动删除
8.2 自增和追加
put 插入数据
get
获取数据
自增
create 'count','info'
默认增加
1
写负数就是自减
incr 'count','row1','info:age',10
获取值
get_counter 'count','row1','info:age'
追加
create 'score','info'put 'score','row1','info:scores','100'
在列值上追加新的内容
append 'score','row1','info:scores',',80'
8.3 新的数据排在前面的方法
001_20180503
001_20200731
001_20200801
001_20200802
默认
rowkey
是字典升序
索引新的数据会被排在后面
怎么让新的数据排在前面来?
大数减小数
Long.MAX_VALUE
30000000 - 20200802
时间越大减完之后就越小,就会排在前面
001_9799198
001_9799199
001_9799200
查询
20200801
的数据
3634

被折叠的 条评论
为什么被折叠?



