HBase--分布式数据库

11 Feb 2021

HBase简介

HBase是什么

HBase 基于 Google的BigTable论文而来,是一个分布式海量列式非关系型数据库系统,可以提供超大规模数据集的实时随机读写

HBase的特点

HBase的应用

HBase适合海量明细数据的存储,并且后期需要有很好的查询性能(单表超千万、上亿,且并发要求高)

HBase数据模型

HBase逻辑架构

HBase物理存储

HBase整体架构

HBase集群安装部署

兼容性问题,这里使用hbase 1.3.1

1. 下载安装包到/opt/lagou/server

cd /opt/lagou/server  
wget http://archive.apache.org/dist/hbase/1.3.1/hbase-1.3.1-bin.tar.gz  

2. 解压安装包到指定的规划⽬目录

tar -zxvf hbase-1.3.1-bin.tar.gz -C /opt/lagou/servers  

3. 修改配置文件

4. 分发hbase目录和环境变量量到其他节点

rsync-script hbase-1.3.1  

5. HBase集群的启动和停⽌

在hbase的bin目录下

# 启动HBase  
start-hbase.sh  
# 停止HBase  
stop-hbase.sh  

6. web端管理界面

启动好HBase集群之后,可以访问地址:
HMaster的主机名:16010

常用hbase shell指令

在HBase 的 bin 目录下进入shell

hbase shell  

help

list

列出当前namespace下面的table

create

建表指令

# 创建一张lagou表, 包含base_info、extra_info两个列族  
create 'lagou', 'base_info', 'extra_info'  
  
# 创建只有三个版本的表格  
# VERSIONS 是指此单元格内的数据可以保留最近的 3 个版本  
create 'lagou', {NAME => 'base_info', VERSIONS => '3'},{NAME => 'extra_info',VERSIONS => '3'}  

put

添加数据

put 'lagou', 'rk1', 'base_info:name', 'wang'  
put 'lagou', 'rk1', 'base_info:age', 30  
put 'lagou', 'rk1', 'extra_info:address', 'shanghai'  

get

查询数据

# 查询数据  
get 'lagou', 'rk1'  
# 查询列族下面的数据  
get 'lagou', 'rk1', 'base_info'  
get 'lagou', 'rk1', 'base_info', 'extra_info'  
# 查询指定列的数据  
get 'lagou', 'rk1', 'base_info:name'  
get 'lagou', 'rk1', 'base_info:name', 'base_info:age'  
get 'lagou', 'rk1', {COLUMN => ['base_info', 'extra_info']}  
get 'lagou', 'rk1', {COLUMN => ['base_info:name', 'extra_info:address']}  
  
# 指定rowkey与列值查询  
# 获取表中row key为rk1,cell的值为wang的信息  
get 'lagou', 'rk1', {FILTER => "ValueFilter(=, 'binary:wang')"}  
  
# 获取表中row key为rk1,列标示符中含有r的信息  
get 'lagou', 'rk1', {FILTER => "QualifierFilter(=, 'substring:r')"}  

scan

查询所有数据

# 查询lagou表中的所有信息  
scan 'lagou'  
# 查询表中列族为 base_info 的信息  
scan 'lagou', {COLUMNS => 'base_info'}  
scan 'lagou', {COLUMNS => 'base_info', RAW => true, VERSIONS => 3}  
## Scan时可以设置是否开启Raw模式,开启Raw模式会返回包括已添加删除标记但是未实际删除的数据   
## VERSIONS指定查询的最大版本数  
  
# 查询lagou表中列族为 base_info 和 extra_info且列标示符中含有r字符的信息  
scan 'lagou', {COLUMNS => ['base_info', 'extra_info'], FILTER => "QualifierFilter(=, 'substring:r')"}  
  
# rowkey的范围值查询(非常重要)  
# 查询lagou表中列族为base_info,rk范围是[rk1, rk3)的数据(rowkey底层存储是字典序)  
# 按rowkey顺序存储。  
scan 'lagou', {COLUMNS => 'base_info', STARTROW => 'rk1', ENDROW => 'rk3'}  
  
# 指定rowkey模糊查询  
# 查询lagou表中row key以rk字符开头的  
scan 'lagou', {FILTER => "PrefixFilter('rk')"}  

put 更新

# 更新操作同插入操作一模一样,只不过有数据就更新,没数据就添加  
# 把lagou表中rowkey为rk1的base_info列族下的列name修改为liang  
put 'lagou', 'rk1', 'base_info:name', 'liang'  

delete

# 指定rowkey以及列列名进⾏删除  
# 删除lagou表row key为rk1,列标示符为 base_info:name 的数据  
delete 'lagou', 'rk1', 'base_info:name'  

alter

删除列族

alter 'lagou', 'delete' => 'base_info'  

truncate

清空表数据

# 删除lagou表数据  
truncate 'lagou'  

drop

删除表

# 删除lagou表  
# 先disable 然后drop  
disable 'lagou'  
drop 'lagou'  

HBase原理

HBase读数据流程

1)首先从zk找到meta表的region位置,然后读取meta表中的数据,meta表中存储了用户表的region信息
2)根据要查询的namespace、表名和rowkey信息。找到写入数据对应的region信息
3)找到这个region对应的regionServer,然后发送请求
4)查找对应的region
5)先从memstore查找数据,如果没有,再从BlockCache上读取
HBase上Regionserver的内存分为两个部分

6)如果BlockCache中也没有找到,再到StoreFile上进行读取
从storeFile中读取到数据之后,不是直接把结果数据返回给客户端,而是把数据先写入到BlockCache中,目的是为了加快后续的查询;然后在返回结果给客户端。

HBase写数据流程

1)首先从zk找到meta表的region位置,然后读取meta表中的数据,meta表中存储了用户表的region信息
2)根据namespace、表名和rowkey信息。找到写入数据对应的region信息
3)找到这个region对应的regionServer,然后发送请求
4)把数据分别写到HLog (write ahead log)和memstore各一份
5)memstore达到阈值后把数据刷到磁盘,生成storeFile文件, storeFile会定期合并,以提高查询速度
6)删除HLog中的历史数据

HBase的flush(刷写)及compact(合并)机制

Region 拆分机制

Region中存储的是⼤量的rowkey数据 ,当Region中的数据条数过多的时候,直接影响查询效率.当Region过⼤的时候.HBase会拆分Region , 这也是Hbase的⼀个优点 .

HBase表的预分区(region)

Region合并

Region的合并不是为了性能,而是出于维护的目的。

HBase API客户端操作

1. 添加依赖

<dependencies>  
    <dependency>  
        <groupId>org.apache.hbase</groupId>  
        <artifactId>hbase-client</artifactId>  
        <version>1.3.1</version>  
    </dependency>  
    <dependency>  
        <groupId>junit</groupId>  
        <artifactId>junit</artifactId>  
        <version>4.12</version>  
        <scope>test</scope>  
    </dependency>  
    <dependency>  
        <groupId>org.testng</groupId>  
        <artifactId>testng</artifactId>  
        <version>6.14.3</version>  
        <scope>test</scope>  
    </dependency>  
    <dependency>  
        <groupId>junit</groupId>  
        <artifactId>junit</artifactId>  
        <version>4.13.1</version>  
        <scope>compile</scope>  
    </dependency>  
</dependencies>  

2. 初始化及收尾

public class HBaseClientDemo {  
    private HBaseConfiguration configuration = null;  
    private Connection connection = null;  
    // 封装了调整表结构相关的操作DDL, DML  
    private HBaseAdmin admin = null;  
    // 后面会重复使用table  
    private Table teacher = null;  
  
    @Before  
    public void init() throws IOException {  
//        创建hbase链接  
        configuration = new HBaseConfiguration();  
        // zookeeper所在节点,注意:服务器之间不要加空格  
        configuration.set("hbase.zookeeper.quorum", "centos7-1,centos7-2");  
        configuration.set("hbase.zookeeper.property.clientPort", "2181");  
        connection = ConnectionFactory.createConnection(configuration);  
        teacher = connection.getTable(TableName.valueOf("teacher"));  
    }  
  
  
@After  
public void destroy() {  
    if (teacher != null) {  
        try {  
            teacher.close();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
  
    if (admin != null) {  
        try {  
            admin.close();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
  
    if (connection != null) {  
        try {  
            connection.close();  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
}  
}  

3. 创建表

@Test  
public void createTable() throws IOException {  
    HBaseAdmin admin = (HBaseAdmin)connection.getAdmin();  
  
    // table描述类  
    HTableDescriptor teacher = new HTableDescriptor(TableName.valueOf("teacher"));  
    // 添加列族  
    teacher.addFamily(new HColumnDescriptor("info"));  
    // 创建表格  
    admin.createTable(teacher);  
  
    System.out.println("create teacher success");  
}  

4. 添加、更新数据

@Test  
public void putData() throws IOException {  
    Put put = new Put(Bytes.toBytes("110"));  
    put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("name"), Bytes.toBytes("April"));  
  
    teacher.put(put);  
  
    System.out.println("put data success");  
}  

5. 删除数据

@Test  
public void deleteData() throws IOException {  
    Delete delete = new Delete(Bytes.toBytes("110"));  
  
    teacher.delete(delete);  
  
    System.out.println("delete data success");  
}  

6. 根据列查询数据

@Test  
public void getDataByCF() throws IOException {  
    Get get = new Get(Bytes.toBytes("110"));  
  
    get.addFamily(Bytes.toBytes("info"));  
  
    Result result = teacher.get(get);  
  
    List<Cell> cells = result.listCells();  
    for (Cell cell : cells) {  
        String row = Bytes.toString(CellUtil.cloneRow(cell));  
        String family = Bytes.toString(CellUtil.cloneFamily(cell));  
        String column = Bytes.toString(CellUtil.cloneQualifier(cell));  
        String value = Bytes.toString(CellUtil.cloneValue(cell));  
  
        System.out.println("row:" + row + "\t"  
                + "family:" + family + "\t"  
                + "column:" + column + "\t"  
                + "value:" + value  
        );  
    }  
}  

7. 扫描全局数据

@Test  
public void scanAllData() throws IOException {  
    Scan scan = new Scan();  
    ResultScanner scanner = teacher.getScanner(scan);  
    for (Result result : scanner) {  
  
        for (Cell cell : result.listCells()) {  
            String row = Bytes.toString(CellUtil.cloneRow(cell));  
            String family = Bytes.toString(CellUtil.cloneFamily(cell));  
            String column = Bytes.toString(CellUtil.cloneQualifier(cell));  
            String value = Bytes.toString(CellUtil.cloneValue(cell));  
  
            System.out.println("row:" + row + "\t"  
                    + "family:" + family + "\t"  
                    + "column:" + column + "\t"  
                    + "value:" + value  
            );  
        }  
    }  
  
    scanner.close();  
}  

8. 限定key区间扫描

@Test  
public void scanRowKey() throws IOException {  
  
    Scan scan = new Scan();  
    scan.setStartRow(Bytes.toBytes("001"));  
    scan.setStopRow(Bytes.toBytes("111"));  
  
    ResultScanner scanner = teacher.getScanner(scan);  
    for (Result result : scanner) {  
  
        for (Cell cell : result.listCells()) {  
            String row = Bytes.toString(CellUtil.cloneRow(cell));  
            String family = Bytes.toString(CellUtil.cloneFamily(cell));  
            String column = Bytes.toString(CellUtil.cloneQualifier(cell));  
            String value = Bytes.toString(CellUtil.cloneValue(cell));  
  
            System.out.println("row:" + row + "\t"  
                    + "family:" + family + "\t"  
                    + "column:" + column + "\t"  
                    + "value:" + value  
            );  
        }  
    }  
  
    scanner.close();  
}  

Hbase 协处理器

官方地址: http://hbase.apache.org/book.html#cp

访问HBase的方式是使用scan或get获取数据,在获取到的数据上进行业务运算。但是在数据量非常大的时候,比如一个有上亿行及十万个列的数据集,再按常用的方式移动获取数据就会遇到性能问题。客户端也需要有强大的计算能力以及足够的内存来处理这么多的数据。
此时就可以考虑使用Coprocessor(协处理器)。将业务运算代码封装到Coprocessor中并在RegionServer上运行,即在数据实际存储位置执行,最后将运算结果返回到客户端。利用协处理器,用户可以编写运行在HBase Server端的代码。

Observer

协处理器与触发器(trigger)类似:在一些特定事件发生时回调函数(也被称作钩子函数,hook)被执行。这些事件包括一些用户产生的事件,也包括服务器端内部自动产生的事件。

Endpoint

这类协处理器类似传统数据库中的存储过程,客户端可以调用这些Endpoint协处理器在Regionserver中执行一段代码,并将RegionServer端执行结果返回给客户端进一步处理。

Observer 案例

需求
通过协处理器Observer实现Hbase当中t1表插入数据,指定的另一张表t2也需要插入相对应的数据。

create 't1','info'  
create 't2’,’info'  

实现思路
通过Observer协处理器捕捉到t1插入数据时,将数据复制一份并保存到t2表中

HBase表的RowKey设计

rowkey大小顺序

ASCII码字典顺序,即字符串比较的顺序

RowKey长度原则

rowkey是一个二进制码流,可以是任意字符串,最大长度64kb,实际应用中一般为10-100bytes,以byte[]形式保存,一般设计成定长。 建议越短越好,不要超过16个字节设计过长会降低memstore内存的利用率和HFile存储数据的效率。

RowKey散列原则

建议将rowkey的高位作为散列字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。

RowKey唯一原则

必须在设计上保证其唯一性,访问hbase table中的行有3种方式:

RowKey排序原则

HBase的Rowkey是按照ASII有序设计的,我们在设计Rowkey时要充分利用这点

HBase表的热点及解决方案

检索habse的记录首先要通过row key来定位数据行。当大量的client访问hbase集群的一个或少数几个节点,造成少数region server的读/写请求过多、负载过大,而其他region server负载却很小,就造成了”热点”现象

预分区

预分区的目的让表的数据可以均衡的分散在集群中,而不是默认只有一个region分布在集群的一个节点上

加盐

这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不同。
4个region, [a)[a,b),[b,),[c,]
原始数据:abc1,abc2,abc3.
加盐后的rowkey: a-abc1,b-abc2,c-abc3

哈希

哈希会使同一行永远用一个前缀加盐。哈希也可以使负载分散到整个集群,但是读却是可以预测的。使用确定的哈希可以让客户端重构完整的rowkey,可以使用get操作准确获取某一个行数据。

原始数据:abc1, abc2,abc3
哈希:
md5(abc1) 9223…..9223-abc1
md5(abc2) 321111..32a1-abc2
md5(abc3) 452…. 452b-abc3.

反转

反转固定长度或者数字格式的rowkey。这样可以使得rowkey中经常改变的部分(最没有意义的部分)放在前面。这样可以有效的随机rowkey,但是牺牲了rowkey的有序性。
15X,13X

HBase的二级索引

HBase表按照rowkey查询性能是最高的。rowkey就相当于hbase表的一级索引!!
为了HBase的数据查询更高效、适应更多的场景,诸如使用非rowkey字段检索也能做到秒级响应,或者支持各个字段进行模糊查询和多字段组合查询等,因此需要在HBase上面构建二级索引,以满足现实中更复杂多样的业务需求。
hbase的二级索引其本质就是建立hbase表中列与行键之间的映射关系。
常见的二级索引我们一般可以借助各种其他的方式来实现,例如Phoenix或者solr或者ES等

布隆过滤器在hbase的应用

hbase的读操作需要访问大量的文件,大部分的实现通过布隆过滤器来避免大量的读文件操作

布隆过滤器的原理

通常判断某个元素是否存在用的可以选择hashmap。但是HashMap的实现也有缺点,例如存储容量占比高,考虑到负载因子的存在,通常空间是不能被用满的,而一旦你的值很多例如上亿的时候,那 HashMap占据的内存大小就变得很可观了。
Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。 hbase中布隆过滤器来过滤指定的rowkey是否在目标文件,避免扫描多个文件。使用布隆过滤器来判断。
布隆过滤器返回true,在结果不一定存在, 如果返回false则说明确实不存在。

Bloom Filter案例