HBase学习

文章中示例源代码地址:文章源代码

1. HBase

1.1 HBase简介

HBase简介:

  • Hadoop DataBase,是一个高可靠性、高性能、面向列、可伸缩、实时读写的分布式数据库
  • 利用Hadoop HDFS作为文件存储系统,利用Hadoop MapReduce来处理HBase中的海量数据,利用Zookeeper作为其分布式协同服务
  • 主要用来存储非结构化和半结构化的松散数据(列存NoSQL数据库)

1.2 HBase数据模型

HBase数据模型由Row Key、Column Family列族和qualifer列、TimeStamp时间戳、Cell数据单元格以及HLog(WAL Log)组成:

  • ROW KEY
    • 决定一行数据
    • 按照字典顺序排列的
    • Row Key只能存储64K的字节数据
  • Column Family列族-qualifier列
    • HBase表中的每个列都归属于某个列族,列族必须作为表模式(schema)定义的一部分预先给出,例如:create 'test' 'course'
    • 列名以列族作为前缀,每个列族都可以有很多个列成员(column);如course:math,course:English,新的列族成员(列)可以随后按需、动态加入
    • 权限控制、存储以及调优都是在列族层面进行的
    • HBase把同一列族里面的数据存储在同一目录下,由几个文件保存
  • TimeStamp时间戳
    • 在HBase每个cell存储单元对同一份数据有多个版本,根据唯一的时间戳来区分每个版本之间的差异,不同版本的数据按照时间倒序排序,最新的数据版本排在最前面。
    • 时间戳的类型是64为整型
    • 时间戳可以由HBase(在数据写入时自动)赋值,此时间戳是精确到毫秒的当前系统时间。
    • 时间戳也可以由客户显式赋值,如果应用程序要避免数据版本冲突,就必须自己生成具有唯一性的时间戳
  • Cell单元格
    • 由行和列的坐标交叉决定
    • 单元格是有版本的
    • 单元格的内容是未解析的字节数组
      • {row key, column(=<family>+<qualifier>),version}唯一确定单元
      • cell中的数据都是没有类型的,全部是字节码形式存储
  • HLog(WAL Log):
    • HLog文件就是一个普通的Hadoop Sequence File,Sequence File的key是HLogKey对象,HLogKey中记录了写入数据的归属信息,除了table和region名字外,同时还包括sequence number和timestamp,timestamp是写入时间,sequence number的起始值为0,或者是最近一次存入文件系统中sequence number
    • HLog SequenceFile的Value是HBase的KeyValue对象,即对应HFile中的KeyValue

1.3 HBase架构

在这里插入图片描述

  • Client
    • 包含访问HBase的接口并维护cache来加快对HBase的访问
  • Zookeeper
    • 保证任何时候,集群中只有一个master
    • 存储所有region的寻址入口
    • 实时监控Region Server的上线和下线信息,并实时通知Master
    • 存储HBase的schema和table元数据
  • Master
    • 为Region Server分配Region
    • 负责Region Server的负载均衡
    • 发现失效的Region Server并重新分配其上的region
    • 管理用户对table的增删改操作
  • RegionServer
    • Region Server负责维护Region,处理对这些region的IO请求
    • Region server负责切分在运行过程中变得过大的region
  • Region
    • HBase自动把表水平划分成多个区域(region),每个region会保存一个表里面某段连续的数据
    • 每个表一开始只有一个region,随着数据不断插入表,region会不断增大, 当增大到一个阀值的时候,region就会等分成两个新的region(裂变)
    • 当table中的行不断增多,就会有越来越多的region,这样一张完整的表就被保存在多个region上了
  • MemStore与storeFile
    • 一个region由多个store组成,一个store对应一个CF(列族)
    • store包括位于内存中的memstore和位于磁盘的storefile写操作先写入memstore(相当于转存),当memstore中的数据达到某个阈值的时候,hregionserver会启动flashcache进程写入storefile,每次写入形成单独的一个storefile
    • 当storefile文件的数量增长到一定的阈值后,系统会进行合并(minor,major,compaction),在合并过程中会进行版本合并和删除工作(major),形成更大的storefile
    • 当一个region中所有的storefile的大小和数量超过一定的阈值后,会把当前的region分割为两个,并由hmaster 分配到相应的regionserver服务器,实现负载均衡
    • 客户端检索数据时,先在memstore中查找,找不到再查找storefile

HRegion是HBase中分布式存储和负载均衡的最小单元,最小单元就表示 不同的HRegion可以分布在不同的HRegion Server上
HRegion由一个或者多个Store组成,每个store保存一个columns family
每个Store又由一个memStore和0至多个StoreFile组成。如图:StoreFile以File格式保存在HDFS上
在这里插入图片描述
HBase的整体架构
在这里插入图片描述

2. HBase搭建

2.1 HBase的伪分布式搭建(单节点)

HBase的下载地址:国内镜像很快:HBase下载
上传到linux上,解压缩即可,注意此时下载的HBase需要的是jdk1.8+
解压缩之后,进入到/conf目录,修改配置文件hbase-env.sh中修改JAVA_HOME的地址路径
修改配置文件hbase-site.xml如下:

<property>
    <name>hbase.rootdir</name>
    <value>file:///home/testuser/hbase</value>
  </property>
  <property>
    <name>hbase.zookeeper.property.dataDir</name>
    <value>/home/testuser/zookeeper</value>
  </property>
  <property>
    <name>hbase.unsafe.stream.capability.enforce</name>
    <value>false</value>
  </property>

配置/etc/profile中的HBASE_HOME即可
启动hbase shell
一定要记得启动hbasestart-hbase.sh否则就会出现进入了hbase只有,什么都执行不了,而且巨慢,最后告诉你master不存在!!!!!

进入到hbase shell中以后,就可以开始来学习基本的语法了

create 'test', 'cf'   // 创建表test,列为cf

describe 'test'       // 查看表test的详情

list 'test'           // 可以使用list来确认表test存在

disable 'test'        // 设置表test不可用

enable 'test'         // 设置表可用

drop 'test'           // 删除表test。注意enabled状态的表系统会提示不可删除

put 'test','row1','cf:name','caoduanxi' // 向表中放入数据,更新也是这样,直接覆盖

scan 'test'           // 浏览表信息

get 'test','row1'     // 获取表中rowkey为row1的所有数据

2.2 完全分布式集群的搭建

之前hadoop配置的节点位置
在这里插入图片描述
现在RegionServers 配置在node02 node03 node04上
HMater配置在node01 node05上

下载然后传送到安装包到linux上,注意都先放在node01上配置,后面再分发即可

免秘钥配置:之前没有配置过的可以配置

ssh-keygen
ssh-copy-id -i /root/.ssh/id_rsa.pub node01   // 这是添加对方的秘钥,需要相互添加

时间同步配置:采用时间同步服务器ntp
注意,在此处,如果不同步时间的话,HRegionServer的启动,将会在启动不就早就自动挂掉,这是后面访问web端进行可视化界面可以发现的问题

yum install -y ntp    // 安装,所有结点都需要安装
ntpdate ntp1.aliyun.com  // 采用的阿里云同步,如果失效,网上百度即可

进入到解压安装目录:
cd /conf修改配置文件hbase-env.sh其中修改JAVA_HOME以及停用HBase自身带的Zookeeper服务export HBASE_MANAGES_ZK=false即可

修改配置文件hbase-site.xml,内容如下

<property>
    <name>hbase.master.info.port</name>
    <value>60010</value>
</property>
<!-- 这里注意需要修改为自己hdfs的目录 -->
 <property>
    <name>hbase.rootdir</name>
    <value>hdfs://mycluster:8020/hbase</value>
  </property>
  <property>
    <name>hbase.cluster.distributed</name>
    <value>true</value>
  </property>
  <property>
    <name>hbase.zookeeper.quorum</name>
    <value>node02,node03,node04</value>
  </property>
<property>
	<name>hbase.unsafe.stream.capability.enforce</name>
	<value>false</value>
</property>

配置regionServers

// 由于声明了RegionServers的配置在node02 node03 node04上面这里配置为
node02
node03
node04

最后配置backup-masters:如果conf中,没有这个文件,自己新建一个然后配置

node05

最后启动服务即可!访问node01:60010即可查看到如下信息:
###在这里插入图片描述

2.3 常见错误解决

在完全分布式配置的过程中会出现一些错误:
如果启动正常,访问网址不出,查看日志,看启动的详情信息

2.3.1 错误1

java.lang.NoClassDefFoundError: org/apache/htrace/SamplerBuilder
解决办法:
这主要是类找不到了,也就是jar包找不到了,只需要将第三方的jar包拷贝一份到我们的lib下即可
cp htrace-core-3.1.0-incubating.jar $HBASE_HOME/lib即可解决

2.3.2 错误2

NullPointerException
解决办法:在配置文件hbase-site.xml中添加配置

<property>
	<name>hbase.unsafe.stream.capability.enforce</name>
	<value>false</value>
</property>
2.3.3 错误3

java.lang.RuntimeException: HRegionServer Aborted
此时就是HRegionServer挂掉的问题了
解决办法:就是安装ntp,使用时间同步服务器同步各个RegionServer节点即可

2.3.4 错误4

java.lang.RuntimeException: Failed construction of Master: class org.apache.hadoop.hbase.master.HMaster
如果启动后hbase以后HMaster很快就挂掉,那么就将hadoop下hdfs-site.xml core-site.xml拷贝到hbase/conf下即可

3. HBase的JavaAPI

准备工作:添加pom依赖,以及HBase的配置文件:

<dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-client</artifactId>
      <version>2.1.3</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-common</artifactId>
      <version>2.1.3</version>
    </dependency>
    <dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>2.1.3</version>
    </dependency>

配置文件主要就是Hbase的conf目录下的:hbase-site.xml即可

HBase的javaAPI学习,直接上代码
主要是介绍HBase中表的创建,其中在初始化阶段由于我使用的是2.1.3版本的Hbase,很多方法或者类被废弃,需要提供新的方法或者类来支持HBase的JavaAPI,表的内容插入,其中修改表HBase是采用覆盖的方式,于是和插入的方法差不多。HBase的数据获取,数据的过滤以及数据的浏览。

/**
 * @author caoduanxi
 * @2019/10/5 10:51
 * HBase的javaAPI的认识
 */
public class HBaseDDL {
    Connection connection;
    public Admin admin;
    String tableName = "phone";

    /**
     * 实现HBase的初始化操作
     */
    @Before
    public void init() throws Exception {
        Configuration conf = new Configuration();
        conf.set("hbase.zookeeper.quorum", "node02,node03,node04");
        connection = ConnectionFactory.createConnection(conf);
        // 构建HBaseAdmin
        admin = connection.getAdmin();
    }

    /**
     * 创建表
     */
    @Test
    public void createTable() throws Exception {
        // 如果表存在则先删除表,然后创建
        if (admin.tableExists(TableName.valueOf(tableName))) {
            admin.disableTable(TableName.valueOf(tableName));
            admin.deleteTable(TableName.valueOf(tableName));
        }
        // 获取表描述
        TableDescriptorBuilder descriptor = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));

        // 获取列描述
        ColumnFamilyDescriptor cd = ColumnFamilyDescriptorBuilder.newBuilder("cf".getBytes()).build();
        // 如果是多个的话,采用familyNames遍历的形式
//        for (String familyName : familyNames) {
//
//        }
        descriptor.setColumnFamily(cd);
        admin.createTable(descriptor.build());
    }

    /**
     * 添加数据/修改数据是直接覆盖,等同
     */
    @Test
    public void putData() throws Exception {
        // 首先获取到表
        Table table = connection.getTable(TableName.valueOf("test01"));
        String rowKey = "row1";
        Put put = new Put(rowKey.getBytes());
        put.addColumn("cf".getBytes(), "name".getBytes(), "caoduanxi".getBytes());
        put.addColumn("cf".getBytes(), "age".getBytes(), "23".getBytes());
        put.addColumn("cf".getBytes(), "sex".getBytes(), "boy".getBytes());
        table.put(put);
    }

    /**
     * 获取数据
     *
     * @throws Exception
     */
    @Test
    public void getData() throws Exception {
        // 使用get可以获取任何的单行数据
        Get get = new Get("row1".getBytes());
        get.addColumn("cf".getBytes(), "name".getBytes());
        get.addColumn("cf".getBytes(), "age".getBytes());
        get.addColumn("cf".getBytes(), "sex".getBytes());
        // 获取表
        Table table = connection.getTable(TableName.valueOf("test01"));
        Result result = table.get(get);
        // 获取cell单元格中的数据
        Cell cell1 = result.getColumnLatestCell("cf".getBytes(), "name".getBytes());
        Cell cell2 = result.getColumnLatestCell("cf".getBytes(), "age".getBytes());
        Cell cell3 = result.getColumnLatestCell("cf".getBytes(), "sex".getBytes());
        System.out.println(new String(CellUtil.cloneValue(cell1)));
        System.out.println(new String(CellUtil.cloneValue(cell2)));
        System.out.println(new String(CellUtil.cloneValue(cell3)));
    }

    Random random = new Random();
    SimpleDateFormat sf = new SimpleDateFormat("yyyyMMddHHmmss");

    /**
     * 通话记录存储表
     * 十个用户,每个用户每天产生100条通话记录
     *
     * @throws Exception
     */
    @Test
    public void insertDB() throws Exception {
        List<Put> puts = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            String phoneNum = getPhoneNum("157");
            for (int j = 0; j < 100; j++) {
                String dnum = getPhoneNum("132");
                String length = random.nextInt(99) + "";
                String type = random.nextInt(2) + "";
                String dateStr = getDateStr("2019");
                // 设计rowkey,实现按照时间倒序排列
                String rowKey = phoneNum + "_" + (Long.MAX_VALUE - sf.parse(dateStr).getTime());
                Put put = new Put(rowKey.getBytes());
                put.addColumn("cf".getBytes(), "dnum".getBytes(), dnum.getBytes());
                put.addColumn("cf".getBytes(), "length".getBytes(), length.getBytes());
                put.addColumn("cf".getBytes(), "type".getBytes(), type.getBytes());
                put.addColumn("cf".getBytes(), "dateStr".getBytes(), dateStr.getBytes());
                puts.add(put);
            }
        }
        Table table = connection.getTable(TableName.valueOf("phone"));
        table.put(puts);
    }

    /**
     * 获取随机时间字符串
     *
     * @param s
     * @return
     */
    private String getDateStr(String s) {
        return s + String.format("%02d%02d%02d%02d%02d", new Object[]{random.nextInt(12) + 1,
                random.nextInt(30) + 1, random.nextInt(24) + 1, random.nextInt(60), random.nextInt(60)
        });
    }

    @Test
    public void test() {
        System.out.println(getDateStr("2019"));
        System.out.println(getDateStr("2018"));
    }

    /**
     * 获取随机的电话号码
     *
     * @param s
     * @return
     */
    private String getPhoneNum(String s) {
        return s + String.format("%08d", random.nextInt(99999999));
    }

    /**
     * 统计二月份到三月份的通话记录
     */
    @Test
    public void scan() throws Exception {
        String phoneNum = "15798412711";
        String startRow = phoneNum + "_" + (Long.MAX_VALUE - sf.parse("20190301000000").getTime());
        String stopRow = phoneNum + "_" + (Long.MAX_VALUE - sf.parse("20190201000000").getTime());

        Scan scan = new Scan();
        scan.withStartRow(startRow.getBytes());
        scan.withStopRow(stopRow.getBytes());

        Table table = connection.getTable(TableName.valueOf("phone"));
        ResultScanner results = table.getScanner(scan);
        for (Result result : results) {
            System.out.print(new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "dnum".getBytes()))));
            System.out.print("-" + new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "length".getBytes()))));
            System.out.print("-" + new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "type".getBytes()))));
            System.out.println("-" + new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "dateStr".getBytes()))));
        }
    }

    /**
     * 获取某号码的type为1的通话记录
     */
    @Test
    public void getType() throws Exception {
        String phoneNum = "15798412711";
        // 前缀过滤器
        PrefixFilter filter = new PrefixFilter(phoneNum.getBytes());
        FilterList list = new FilterList(FilterList.Operator.MUST_PASS_ALL);
        // 单列值选择过滤器
        SingleColumnValueFilter filter1 = new SingleColumnValueFilter(
                "cf".getBytes(),
                "type".getBytes(),
                CompareOperator.EQUAL,
                "1".getBytes()
        );
        // 添加过滤器
        list.addFilter(filter);
        list.addFilter(filter1);
        Scan scan = new Scan();
        scan.setFilter(list);
        Table table = connection.getTable(TableName.valueOf("phone"));
        ResultScanner results = table.getScanner(scan);
        for (Result result : results) {
            System.out.print(new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "dnum".getBytes()))));
            System.out.print("-" + new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "length".getBytes()))));
            System.out.print("-" + new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "type".getBytes()))));
            System.out.println("-" + new String(CellUtil.cloneValue(result.getColumnLatestCell("cf".getBytes(), "dateStr".getBytes()))));
        }

    }

    /**
     * 删除表
     */
    @Test
    public void deleteTable() throws Exception {
        Table table = connection.getTable(TableName.valueOf("test01"));
        Delete delete = new Delete("row1".getBytes());
        table.delete(delete);
    }


    @After
    public void destroy() throws IOException {
        if (admin != null) {
            admin.close();
        }
    }
}

4. 思考练习-如何设计rowkey

4.1 人员-角色

1、人员-角色
   人员有多个角色 角色优先级
   角色有多个人员
   人员 删除添加角色
   角色 删除添加人员
   人员 角色 删除添加

首先需要设计两张表,一张表存储人员信息,同时存储自身包含的角色信息;第二张表存储角色信息,同时存储自身包含的人员信息。

		 8 9 10 代表优先级
		 100		200			300
  小红 > 体育委员 > 劳动委员 > 学习委员
  小明 > 劳动委员 > 学习委员
  
  person			cf1									cf2
  pid=001			cf1:name,cf1:age....				cf2:100=8;cf2:200=9;cf2:300=10
  pid=002			cf1:name,cf1:age..					cf2:200=9;cf2:300=10
  
  
  role				cf1									cf2
  rid=100			cf1:rname=体育委员					cf2:001=小红,cf2:002=小明
  rid=200			cf1:rname=劳动委员					cf2:001=小红;cf2=小明	
  rid=300			cf1:rname=学习委员					cf2:001=小红
  
  添加删除角色,添加删除人员,都需要维护人员表与角色表

4.2 组织架构

2、组织架构 部门-子部门
   查询  顶级部门
   查询  每个部门的所有子部门
   部门 添加、删除子部门
   部门 添加、删除
  (部门表,0代表没有父部门的即顶级部门) 
  department   		cf1(部门)									cf2(子部门)
  did:0_001			cf1:name=boss								cf2:002=研发;cf2:003=测试	
  did:1_002			cf1:name=研发;cf1:fdid=0_001				    cf2:004=大数据
  did:1_003			cf1:name=测试;cf1:fdid=0_001				    cf2:005=测试1
  did:1_004			cf1:name=大数据;cf1:fdid=0_002				cf2:
  同样,删除添加子部门,也需要维护父id中的子部门信息的删除添加

4.3 微博案例

3、微博
  添加、查看关注
  粉丝列表
  写微博
  查看首页,所有关注过的好友发布的最新微博
  查看某个用户发布的所有微博,降序排列
	小明001            关注:小红、小绿
	小红002			
	小黑003            关注:小红
	小绿004		   	  关注:小红
   
  关注表:
  focus                		cf1(关注表)                       	cf2(粉丝表)
  pid:001				  	cf1:002=小红;cf1:004=小绿         
  pid:002 				   										cf2:001=小明;cf2:003=小黑;cf2:004=小绿
  pid:003					cf1:002=小红						
  pid:004					cf1:002=小红						cf2:001=小明
  
  
  写微博(需要倒序排列)
  writeWeiBo									cf1(内容)
  wid:004_(max-timestamp)						cf1:titile="文章";cf1:content="..."
  wid:002_10001									cf1:titile="文章";cf1:content="..."
  wid:004_40001									cf1:titile="文章";cf1:content="..."
	
  微博收录统计表			
  sumWeiBo						cf1(version=10000)
  sid:001						cf1:wid=004_40001
								cf1:wid=002_10001
  小红发微博 -> 写微博表中记录  ->  微博收录表中收录  
  ->  小明此时打开关注好友列表查看小红和小绿最新发布微博
  小明想查看小红发的微博只需要在写微博表中根局小红id的前缀过滤即可查看

5. 使用protobuf简化存储

之前的api学习中,对于通话记录的存储,几乎是每出现一次数据的更新就要存储一遍,rowkey的长度比四个数据的都长,如何简化rowkey的存储成了一个问题。可以将所有rowkey相同的数据都只存一次rowkey,然后再存储数据,也就是将同一个rowkey的数据演变成一个对象存储即可。引入protobuf;

引自百度百科:
protocolbuffer(以下简称PB)是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。

首先在protobuf上下载响应的压缩包,解压缩,./configuremake && make install即可完成安装
proto的使用,需要先建立一个存储对象类:

package com.duanxi.hbase;
  message phoneClass
  {
	required string dnum=1;
	required string length=2;
	required string type=3;
	required string dateStr=4; 
  }

选择linux上的一个存储目录,新建文件vi phoneDetail.proto注意文件名的后缀必须是proto

引自官方文档
编译 .proto 文件
写好 proto 文件之后就可以用 Protobuf 编译器将该文件编译成目标语言了。本例中我们将使用 C++。
假设您的 proto 文件存放在 S R C D I R 下 面 , 您 也 想 把 生 成 的 文 件 放 在 同 一 个 目 录 下 , 则 可 以 使 用 如 下 命 令 : p r o t o c − I = SRC_DIR 下面,您也想把生成的文件放在同一个目录下,则可以使用如下命令: protoc -I= SRCDIR使protocI=SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
命令将生成两个文件:
lm.helloworld.pb.h , 定义了 C++ 类的头文件
lm.helloworld.pb.cc , C++ 类的实现文件
在生成的头文件中,定义了一个 C++ 类 helloworld,后面的 Writer 和 Reader 将使用这个类来对消息进行操作。诸如对消息的成员进行赋值,将消息序列化等等都有相应的方法。

具体操作:我选择的目录是/a/phoneDetail.proto

protoc -I=/a --java_out=/a/ /a/phoneDetail.proto

编译完成之后,文件中会生成具体的com文件夹,其中含有.java的文件,将文件拷贝到Idea上,此时完成代码的编写即可实现HBase数据对象形式的存储。

/**
     * 利用protobuf将插入的数据变为对象存储,简化rowkey
     */
    @Test
    public void insertDB2() throws Exception {
        List<Put> puts = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            String phoneNum = getPhoneNum("157");
            for (int j = 0; j < 100; j++) {
                String dnum = getPhoneNum("132");
                String length = random.nextInt(99) + "";
                String type = random.nextInt(2) + "";
                String dateStr = getDateStr("2019");
                // 设计rowkey,实现按照时间倒序排列
                String rowKey = phoneNum + "_" + (Long.MAX_VALUE - sf.parse(dateStr).getTime());

                // 需要实现一个build将所有的数据插入,然后再利用puts存储
                PhoneDetail.phoneClass.Builder builder = PhoneDetail.phoneClass.newBuilder();
                builder.setDnum(dnum);
                builder.setLength(length);
                builder.setType(type);
                builder.setDateStr(dateStr);
                Put put = new Put(rowKey.getBytes());
                put.addColumn("cf".getBytes(), "phoneDetail".getBytes(), builder.build().toByteArray());
                puts.add(put);
            }

        }
        Table table = connection.getTable(TableName.valueOf("phone"));
        table.put(puts);
    }

结果:
在这里插入图片描述

5.1 protobuf嵌套message实现数据存储

上述protobuf的方法能够解决问题,如果此时10个用户,每个用户产生100条通信记录怎么存储呢?此时需要使用到proto中的嵌套message的方法。来实现类似于java中的list一样的集合来存储。
首先更新之前的proto文件

  package com.duanxi.hbase;
  message phoneClass
  {
	required string dnum=1;
	required string length=2;
	required string type=3;
	required string dateStr=4; 
  }
  message phone
  {
	repeated phoneClass phone=1;
  }

编写存储多个通话记录的程序:包含数据的解析

/**
     * 有10个用户,每个用户每天产生100条数据记录
     * 如何存储,使用protobuf
     */
    @Test
    public void insertDB3() throws Exception {
        List<Put> puts = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            String phoneNum = getPhoneNum("157");
            // 设计rowkey,实现按照时间倒序排列
            String rowKey = phoneNum + "_" + (Long.MAX_VALUE - sf.parse(getDateStr2("20191006")).getTime());
            // 获取存储通话记录的类似java的集合
            PhoneDetail.phone.Builder dayPhone = PhoneDetail.phone.newBuilder();
            for (int j = 0; j < 100; j++) {
                String dnum = getPhoneNum("132");
                String length = random.nextInt(99) + "";
                String type = random.nextInt(2) + "";
                String dateStr = getDateStr("2019");
                // 构建单独存取通话记录的对象
                PhoneDetail.phoneClass.Builder phoneDetail = PhoneDetail.phoneClass.newBuilder();
                phoneDetail.setDnum(dnum);
                phoneDetail.setLength(length);
                phoneDetail.setType(type);
                phoneDetail.setDateStr(dateStr);
                // 往嵌套的message中添加数据
                dayPhone.addPhone(phoneDetail);
            }
            Put put = new Put(rowKey.getBytes());
            put.addColumn("cf".getBytes(),"day".getBytes(),dayPhone.build().toByteArray());
            puts.add(put);
        }
        Table table = connection.getTable(TableName.valueOf("phone"));
        table.put(puts);
    }

    private String getDateStr2(String s) {
        return s + String.format("%02d%02d%02d", new Object[]{random.nextInt(24) + 1,
                random.nextInt(60), random.nextInt(60)
        });
    }

    /**
     * 解析嵌套message中的内容
     */
    @Test
    public void getData2() throws Exception {
        // 获取数据集中的一个rowKey
        String rowKey = "15797250044_9223370466558205807";
        // 构建一个get,获取的是单行的数据
        Get get = new Get(rowKey.getBytes());
        // 获取表
        Table table = connection.getTable(TableName.valueOf("phone"));
        // 从表中获取数据
        Result result = table.get(get);
        // 获取列族单元格中的内容
        Cell cell = result.getColumnLatestCell("cf".getBytes(), "day".getBytes());
        // 需要解析单元格中序列化的内容
        PhoneDetail.phone phone = PhoneDetail.phone.parseFrom(CellUtil.cloneValue(cell));
        for (PhoneDetail.phoneClass phoneDetail : phone.getPhoneList()) {
            System.out.println(phoneDetail);
        }
    }

存储的结果:注意存储的都是序列化之后的字节,占用空间小。
在这里插入图片描述

6. HBase实现MapReduce中的wordCount

需要实现的点:利用MapReduce计算单词的个数,然后将单词的个数存储到HBase数据库中
pom.xml主要依赖:

<dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-common</artifactId>
      <version>0.98.24-hadoop2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-server</artifactId>
      <version>0.98.24-hadoop2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-annotations</artifactId>
      <version>0.98.24-hadoop2</version>
    </dependency>
    <dependency>
      <groupId>org.apache.hbase</groupId>
      <artifactId>hbase-client</artifactId>
      <version>0.98.24-hadoop2</version>
    </dependency>

具体代码实现如下:

public class WCRunner {
    public static void main(String[] args) throws Exception {
        // 获取配置文件
        Configuration conf = new Configuration();
        // 获取配置中的文件
        conf.set("fs.defaultFS","hdfs://node01:8020");
        conf.set("hbase.zookeeper.quorum", "node02,node03,node04");
        // 获取工作对象
        Job job = Job.getInstance(conf);
        // 设置类
        job.setJarByClass(WCRunner.class);
        // 设置mapper
        job.setMapperClass(WCMapper.class);
        // 设置出去的key-value类型
        job.setMapOutputKeyClass(Text.class);
        job.setOutputKeyClass(IntWritable.class);
        // 设置进入的文件路径
        FileInputFormat.addInputPath(job,new Path("/user/hive/warehouse/wc/"));
        // 设置reduce类
        TableMapReduceUtil.initTableReducerJob("wc", WCReducer.class, job, null, null, null, null, false);

        job.waitForCompletion(true);
    }
}
public class WCMapper extends Mapper<LongWritable, Text,Text, IntWritable> {
    @Override
    protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        // map负责的是数据映射写入到context中去
        // 分割获取数据
        String[] split = value.toString().split(" ");
        for (String s : split) {
            context.write(new Text(s),new IntWritable(1));
        }
		// 或者使用以下方法
        /*StringTokenizer string = new StringTokenizer(value.toString());
        while(string.hasMoreTokens()){
            context.write(new Text(string.nextToken()),new IntWritable(1));
        }*/
    }
}
public class WCReducer extends TableReducer<Text, IntWritable, ImmutableBytesWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable value : values) {
            sum += value.get();
        }
        // 需要放入rowkey,其中rowkey直接使用单词来的key就行
        Put put = new Put(key.toString().getBytes());
        put.add("cf".getBytes(),"cnum".getBytes(), Bytes.toBytes(sum));
        context.write(null,put);
    }
}

7. HBase的优化

7.1 表设计的优化

  • region分区设定的优化
  • rowkey设计的优化,在满足业务需求的前提下,越短越好
  • column family列族的设计优化,一般为2~3个,很少超过两个
  • in memory存储在内存的优化
  • time to live存储生命周期的优化
  • compact & split合并以及分割的优化:
    • 在HBase中,数据在更新时首先写入WAL日志(HLog)和内存Memstore中,Memstore中的数据是排序的,当Memstore累积到一定的阈值时,就会创建一个新的Memstore,并且将老的Memstore添加到flush队列中,由单独的线程flush到磁盘上,称为一个storefile,与此同时,系统会在zookeeper中即可一个redo,表示这个时刻之前的更新已经持久化(minor compact)
    • StoreFile是只可读的,一旦创建后就不可以再修改,因此HBase的更新其实是不断追加的过程,当一个stroe中的StroeFile达到一定的阈值以后,就会进行以此合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定的阈值后,又会对StoreFile进行分割(split),等分为两个storeFile
    • 由于对表的更新时不断追加的,处理读请求时,需要访问store中全部的StoreFile和Memstore,将他们按照RowKey进行合并,由于StoreFile和Memstore都是经过排序的,并且StoreFile带有内存索引,通常的合并过程是比较快的。
    • 实际应用中,可以考虑必要时手动进行major compact,将同一个rowkey的修改进行合并形成一个更大的StoreFile,同时可以将StoreFile设置大一些,减少split的发生
    • HBase为了防止小文件(被刷到磁盘的memstore)过多,以保证查询效率,HBase在必要的时候将这些小的storeFile合并成一个较大的StoreFile,这个过程就叫做compaction,主要有minor compaction,major compaction。

7.2 写表优化

  • 多个HTable并发写,创建多个HTable客户端用于写操作,提高写数据的吞吐量
  • HTable参数设置的优化,Auto Flush设置,Write Buffer设置buffer的size大小,WAL(Write Ahead Log)日志的写优化(主要是为数据恢复)。
  • 批量写优化
  • 多线程并发写

7.2 读表优化

  • 多个HTable并发读,创建多个HTable客户端用于读操作,提高读数据的吞吐量
  • HTable参数设置优化(读方面),Scanner Caching 设置从服务端一次抓取的数据的条数,存储在内存中,减少scan过程中next的时间开销
  • Scan Attribute Selection,scan的时候选择列族,可以减少网络传输量,否则scan操作会返回整行的列族的数据
  • Close ResultScanner,通过scan读取完数据之后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的server资源无法释放)
  • 批量读
  • 多线程并发读
  • 缓存查询结果
  • BlockCache:
    • HBase上的RegionServer的内存主要分为两个部分:一个为Memstore,另一个为BlockCache,主要用于读
    • 写请求会先写入Memstore,RegionServer会给每个region分配一个Memstore,当Memstore满了64M时,会启动flush刷新到磁盘中去,当Memstore的总大小超过限制时,会强行启动flush进程,从最大的Memstore开始flush直到低于限制
    • 读请求先到Memstore中查数据,查不到就到BlockCache中去查,再查不到就到磁盘上去查(前面两个都是在内存中查询,速度快),并把读的结果存储在BlockCache中,由于BlockCache采用的是LRU策略,因此BlockCache达到上限后,会启动淘汰机制,淘汰掉最老的一批数据。
    • 一个RegionServer上面有一个BlockCache和N个Memstore,他们的大小之和不能大于等于heapsize*0.8,否则HBase不能启动,默认BlockCache的大小为0.2,而Memstore为0.4,对于注重读响应时间的系统,可以将BlockCache设置的大些,例如BlockCache=0.4,Memstore=0.39,加大缓存的命中概率

8. 小结

HBase学习主要是学习了HBase这种非关系型数据库,HBase是用来处理海量数据的,对于HBase学习了HBase的架构,知道HBase的整体的架构流程:Client和HMaster通过zookeeper寻址找到HRegionServer的地址,以此来访问HRegionServer,RegionServer管理着Region,数据从Region进入,首先是书写HLog日志以及将文件存储到Store中的Memstore,当Memstore中的文件超出了阈值的话,此时会创建一个新的Memstore,同时将之前的Memstore添加到flush队列中去,由单独的线程flush,形成storeFile,StoreFile如果一直追加数据到一定的阈值,会进行合并操作或者split分割操作。StoreFile与HFile差不多,StoreFile就是对HFile做了一个轻量级的封装,底层还是HFile,HFile存储在HDFS上,响应的HDFS上文件又对应着不同的DataNode。学习了HBase的伪分布式搭建以及完全分布式搭建,只要是配置文件的问题,具体参照官网即可。后面学习了HBase的基本语法,在HBase中建表必须带上列族,添加数据必须执行rowkey,数据的获取是根据rowkey可列族来定位具体的数据,学习了HBase的javaapi的实现,实现了HBase的增删改查操作,已经对于HBase的数据存储,如何以对象的形式存储进来,引入了谷歌的protobuf,从而将我们的数据以对象的形式反序列化存储在Hbase中,对于大量的数据,引入了嵌入的message形式(类似于java的list形式)存储,从而接上了rowkey的存储空间。学习了三个案例,人员-角色,组织架构,以及微博案例rowkey的设计,以及表的设置,rowkey设计需要在满足业务需求的前提下,越短越好,设计rowkey至关重要。对于mapreduce的wordcount数据的存储,可以利用HBase来存储。最后是Hbase的优化,对于海量的数据处理,优化就显得十分的重要,对于HBase的优化,首先是表的实际的优化,从region个数、rowkey的设计、列族的设计、合并以及分割等优化,到写表操作,多表并发写,设置相应的写表参数优化,以及读表操作,多表连读,多变并发读,设置读表参数优化等。

大数据学习加油!

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值