HBase学习的第五天--HBase的·拓展-Phenix

六、Phoenix

3、phoenix表映射

默认情况下,直接在hbase中创建的表,通过phoenix是查看不到的

如果需要在phoenix中操作直接在hbase中创建的表,则需要在phoenix中进行表的映射。映射方式有两种:视图映射和表映射

3.1、视图映射

Phoenix创建的视图是只读的,所以只能用来做查询,无法通过视图对源数据进行修改等操作

# hbase shell 进入hbase命令行
hbase shell 

# 创建hbase表
create 'test2','name','company' 

# 插入数据
put 'test2','001','name:firstname','zhangsan1'
put 'test2','001','name:lastname','zhangsan2'
put 'test2','001','company:name','zhangsan3'
put 'test2','001','company:address','zhangsan4'


upsert into TEST values('002','xiaohu','xiaoxiao','zhangsan3','hefei');


# 在phoenix创建视图, primary key 对应到hbase中的rowkey
-- 创建视图之后,查询数据,主键是不需要加上双引号的
-- 其他字段在查询时需要加上双引号,包括表名
# 一开始在原表中取出非主键列时需要加上双引号
select empid from "test2";

create view "test2"(
empid varchar primary key,
"name"."firstname" varchar,
"name"."lastname"  varchar,
"company"."name"  varchar,
"company"."address" varchar
);

CREATE view "students" (
 id VARCHAR NOT NULL PRIMARY KEY, 
 "info"."name" VARCHAR,
 "info"."age" VARCHAR, 
 "info"."gender" VARCHAR,
 "info"."clazz" VARCHAR
);

# 在phoenix查询数据,表名通过双引号引起来
select * from "test";

# 删除视图
drop view "test";

'1500101004','张三',22,'男','理科一班'
put 'students','1500101001','info:name','xiaohu'
put 'students','1500101001','info:age','29'
put 'students','1500101001','info:gender','男'
put 'students','1500101001','info:clazz',"理科一班"

3.2、表映射

使用Apache Phoenix创建对HBase的表映射,有两类:

1) 当HBase中已经存在表时,可以以类似创建视图的方式创建关联表,只需要将create view改为create table即可。

2)当HBase中不存在表时,可以直接使用create table指令创建需要的表,并且在创建指令中可以根据需要对HBase表结构进行显示的说明。

第1)种情况下,如在之前的基础上已经存在了test表,则表映射的语句如下:

create table "test2" (
empid varchar primary key,
"name"."firstname" varchar,
"name"."lastname"varchar,
"company"."name"  varchar,
"company"."address" varchar
)column_encoded_bytes=0;
#表映射在建表时,映射过来的列是已经确定的,不会再改变,
#即使hbase中发生改变,phoenix也不会改变

upsert into "students" values('150011000100','张三','24','男','理科三班');

upsert into  "test"  values('1001','xiaohu','李四','阿里','杭州');

CREATE table "students" (
 id VARCHAR NOT NULL PRIMARY KEY, 
 "info"."name" VARCHAR,
 "info"."age" VARCHAR, 
 "info"."gender" VARCHAR,
 "info"."clazz" VARCHAR
) column_encoded_bytes=0;

# 增加和删除表的字段值是使用单引号
upsert into "students" values('150011000100','zhangsan','24','男','理科三班');

CREATE table  "scores" (
 id VARCHAR NOT NULL PRIMARY KEY, 
 "info"."score" VARCHAR
) column_encoded_bytes=0;

1、过滤出文科一班的学生
select ID as id,"name" as name,"clazz" as clazz from "students" where "clazz"='文科一班';


2、将成绩表做切分转换
select regexp_split(ID,'-')[1] as student_id,regexp_split(ID,'-')[2] as subject_id,"score" as score from "scores";
|         ID         | score |
+--------------------+-------+
| 1500100001-1000001 | 98    |
| 1500100001-1000002 | 5     |
| 1500100001-1000003 | 137   |
| 1500100001-1000004 | 29    |
| 1500100001-1000005 | 85    |
| 1500100001-1000006 | 52    |
| 1500100002-1000001 | 139   |
| 1500100002-1000002 | 102   |
| 1500100002-1000003 | 44    |
| 1500100002-1000004 | 18    |

3、在第二步的基础之上求每个学生的总分
select 
    t1.sid as score_id,
    sum(to_number(t1."score")) as sum_score
from(
    select
    	regexp_split(id,'-')[1] as sid
    	,regexp_split(id,'-')[2] as subject_id
    	,"score" 
    from "scores") t1
group by t1.sid;

4、在第3步的基础上将学生总分成绩降序排序,取前10名
 select 
    t1.sid as score_id,
    sum(to_number(t1."score")) as sum_score
    from(select
    regexp_split(id,'-')[1] as sid
    ,regexp_split(id,'-')[2] as subject_id
    ,"score" 
from "scores") t1
group by t1.sid order by sum_score desc limit 10;

5、在第四步的基础上关联学生信息表获取总分前十名学生的基本信息
select tt1.id
    ,tt1."name"
    ,tt1."age"
    ,tt1."gender"
    ,tt1."clazz"
    ,to_char(tt2.sum_score) as sum_sc
from "students" tt1
join
 (select 
    t1.sid as score_id,
    sum(to_number(t1."score")) as sum_score
    from(select
    regexp_split(id,'-')[1] as sid
    ,regexp_split(id,'-')[2] as subject_id
    ,"score" 
from "scores") t1
group by t1.sid order by sum_score desc limit 10) tt2
on tt1.id=tt2.score_id
order by sum_sc desc;

6、与步骤1的文科学生进行关联
select b1.id as student_id,b1.name as name,b1.clazz as clazz,to_char(b2.sum_score) as sum_score from (select ID as id,"name" as name,"clazz" as clazz from "students" where "clazz"='文科一班') b1 join (select t1.student_id as student_id,sum(to_number(t1.score)) as sum_score from (select regexp_split(ID,'-')[1] as student_id,regexp_split(ID,'-')[2] as subject_id,"score" as score from "scores") t1 group by t1.student_id) b2 on (b1.id=b2.student_id) order by b2.sum_score desc limit 10;

通过这个例子遇到的注意点:
1、切分字符串的函数不是split,而是regexp_split
2、Phoenix中,数组的索引是从1开始的
3、给字段起别名之后的嵌套查询,就不需要再加双引号了,主键本身就可以不用加
4、sum函数中的数据类型必须是数值类型,如果是10的整数倍,会以科学计数法进行标识 580->5.8E+2(5.8*10^2)
5、to_number()转数值  to_char()转字符串

"jdbc:phoenix:master,node2,node3:2181

使用create table创建的关联表,如果对表进行了修改,源数据也会改变,同时如果关联表被删除,源表也会被删除。但是视图就不会,如果删除视图,源数据不会发生改变。

视图映射和表映射的区别***

1、视图映射只能对映射过来的数据做查询操作,不能做增删改操作,而表映射是可以对映射过来的数据做增删改操作的。

2、在删除表时:对于视图映射删除映射表时,原HBase里的表不会被删除,而表映射删除映射表时,HBase中的原表和数据也都同样会被删除。

七、bulkLoad实现批量导入

优点:

  1. 如果我们一次性入库hbase巨量数据,处理速度慢不说,还特别占用Region资源, 一个比较高效便捷的方法就是使用 “Bulk Loading”方法,即HBase提供的HFileOutputFormat类

  2. 它是利用hbase的数据信息按照特定格式存储在hdfs内这一原理,直接生成这种hdfs内存储的数据格式文件,然后上传至合适位置,即完成巨量数据快速入库的办法。配合mapreduce完成,高效便捷,而且不占用region资源,增添负载。(只进行了数据的读操作,所以只需要map任务)

限制:

  1. 仅适合初次数据导入,即表内数据为空,或者每次入库表内都无数据的情况。
  2. HBase集群与Hadoop集群为同一集群,即HBase所基于的HDFS为生成HFile的MR的集群

代码编写:

提前在Hbase中创建好表

生成Hfile基本流程:

  1. 设置Mapper的输出KV类型:

    K: ImmutableBytesWritable(代表行键)

    V: KeyValue (代表cell)

​ 2. 开发Mapper

​ 读取你的原始数据,按你的需求做处理

​ 输出rowkey作为K,输出一些KeyValue(Put)作为V

​ 3. 配置job参数

​ a. Zookeeper的连接地址

​ b. 配置输出的OutputFormat为HFileOutputFormat2,并为其设置参数

​ 4. 提交job

​ 导入HFile到RegionServer的流程

​ 构建一个表描述对象

​ 构建一个region定位工具

​ 然后用LoadIncrementalHFiles来doBulkload操作

pom文件:

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-common</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.hadoop</groupId>
            <artifactId>hadoop-hdfs</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.hbase</groupId>
            <artifactId>hbase-server</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.phoenix</groupId>
            <artifactId>phoenix-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <!-- compiler插件, 设定JDK版本 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                    <source>1.8</source>
                    <target>1.8</target>
                    <showWarnings>true</showWarnings>
                </configuration>
            </plugin>


            <!-- 带依赖jar 插件-->
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>

    </build>

八、HBase中rowkey的设计(重点!!)

HBase的RowKey设计

HBase是三维有序存储的,通过rowkey(行键),column key(column family和qualifier)和TimeStamp(时间戳)这个三个维度可以对HBase中的数据进行快速定位。

HBase中rowkey可以唯一标识一行记录,在HBase查询的时候,有两种方式:

通过get方式,指定rowkey获取唯一一条记录

通过scan方式,设置startRow和stopRow参数进行范围匹配

全表扫描,即直接扫描整张表中所有行记录

rowkey长度原则

rowkey是一个二进制码流,可以是任意字符串,最大长度 64kb ,实际应用中一般为10-100bytes,以 byte[] 形式保存,一般设计成定长。

建议越短越好,不要超过16个字节,原因如下:

数据的持久化文件HFile中是按照KeyValue存储的,如果rowkey过长,比如超过100字节,1000w行数据,光rowkey就要占用100*1000w=10亿个字节,将近1G数据,这样会极大影响HFile的存储效率;

MemStore将缓存部分数据到内存,如果rowkey字段过长,内存的有效利用率就会降低,系统不能缓存更多的数据,这样会降低检索效率。

目前操作系统都是64位系统,内存8字节对齐,控制在16个字节,8字节的整数倍利用了操作系统的最佳特性。

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 - 结束时间

其他一些建议

尽量减少行和列的大小在HBase中,value永远和它的key一起传输的。当具体的值在系统间传输时,它的rowkey,列名,时间戳也会一起传输。如果你的rowkey和列名很大,甚至可以和具体的值相比较,那么你将会遇到一些有趣的问题。HBase storefiles中的索引(有助于随机访问)最终占据了HBase分配的大量内存,因为具体的值和它的key很大。可以增加block大小使得storefiles索引再更大的时间间隔增加,或者修改表的模式以减小rowkey和列名的大小。压缩也有助于更大的索引。

列族尽可能越短越好,最好是一个字符

冗长的属性名虽然可读性好,但是更短的属性名存储在HBase中会更好

# 原数据:以时间戳_user_id作为rowkey
# 时间戳高位变化不大,太连续,最终可能会导致热点问题
1638584124_user_id
1638584135_user_id
1638584146_user_id
1638584157_user_id
1638584168_user_id
1638584179_user_id

# 解决方案:加盐、反转、哈希

# 加盐
# 加上随即前缀,随机的打散
# 该过程无法预测 前缀时随机的
00_1638584124_user_id
05_1638584135_user_id
03_1638584146_user_id
04_1638584157_user_id
02_1638584168_user_id
06_1638584179_user_id

# 反转
# 适用于高位变化不大,低位变化大的rowkey
4214858361_user_id
5314858361_user_id
6414858361_user_id
7514858361_user_id
8614858361_user_id
9714858361_user_id

# 散列 md5、sha1、sha256......
25531D7065AE158AAB6FA53379523979_user_id
60F9A0072C0BD06C92D768DACF2DFDC3_user_id
D2EFD883A6C0198DA3AF4FD8F82DEB57_user_id
A9A4C265D61E0801D163927DE1299C79_user_id
3F41251355E092D7D8A50130441B58A5_user_id
5E6043C773DA4CF991B389D200B77379_user_id

# 时间戳"反转"
# rowkey:时间戳_user_id
# rowkey是字典升序的,那么越新的记录会被排在最后面,不容易被获取到
# 需求:让最新的记录排在最前面

# 大数:9999999999
# 大数-小数

1638584124_user_id => 8361415875_user_id
1638584135_user_id => 8361415864_user_id
1638584146_user_id => 8361415853_user_id
1638584157_user_id => 8361415842_user_id
1638584168_user_id => 8361415831_user_id
1638584179_user_id => 8361415820_user_id

1638586193_user_id => 8361413806_user_id


posted @ 2024-08-18 22:51  shmil  阅读(18)  评论(1编辑  收藏  举报