Hadoop生态圈技术栈---Zookeeper和HBase

1. Zookeeper简介

1.1 Zookeeper是什么?

Zookeeper 是一个分布式协调服务的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,例如怎样避免同时操作同一数据造成脏读的问题。分布式系统中数据存在一致性的问题!!
ZooKeeper 本质上是一个分布式的小文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。
ZooKeeper 提供给客户端监控存储在zk内部数据的功能,从而可以达到基于数据的集群管理。 诸如: 统一命名服务(dubbo)、分布式配置管理(solr的配置集中管理)、分布式消息队列(sub/pub)、分布式锁、分布式协调等功能。

1.2 zookeeper的架构组成


Leader

  • Zookeeper 集群工作的核心角色
  • 集群内部各个服务器的调度者。
  • 事务请求(写操作) 的唯一调度和处理者,保证集群事务处理的顺序性;对于 create,setData, delete 等有写操作的请求,则需要统一转发给leader 处理, leader 需要决定编号、执行操作,这个过程称为一个事务。

Follower

  • 处理客户端非事务(读操作) 请求,
  • 转发事务请求给 Leader;
  • 参与集群 Leader 选举投票 2n-1台可以做集群投票。
  • 此外,针对访问量比较大的 zookeeper 集群, 还可新增观察者角色。

Observer

  • 观察者角色,观察 Zookeeper 集群的最新状态变化并将这些状态同步过来,其对于非事务请求可以进行独立处理,对于事务请求,则会转发给 Leader服务器进行处理。
  • 不会参与任何形式的投票只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力。增加了集群增加并发的读请求。

ZK也是Master/slave架构,但是与之前不同的是zk集群中的Leader不是指定而来,而是通过选举产生。

1.3 Zookeeper 特点

1. Zookeeper:一个领导者(leader:老大),多个跟随者(follower:小弟)组成的集群。
2. Leader负责进行投票的发起和决议,更新系统状态(内部原理)
3. Follower用于接收客户请求并向客户端返回结果,在选举Leader过程中参与投票
4. 集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。
5. 全局数据一致:每个server保存一份相同的数据副本,Client无论连接到哪个server,数据都是一致的。
6. 更新请求顺序进行(内部原理)
7. 数据更新原子性,一次数据更新要么成功,要么失败。


2. Zookeeper环境搭建

2.1 Zookeeper的搭建方式

Zookeeper安装方式有三种,单机模式和集群模式以及伪集群模式。
■ 单机模式:Zookeeper只运行在一台服务器上,适合测试环境;
■ 伪集群模式:就是在一台服务器上运行多个Zookeeper 实例;
■ 集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”

2.2 Zookeeper集群搭建

下载
首先我们下载稳定版本的zookeeper http://zookeeper.apache.org/releases.html

上传
下载完成后,将zookeeper压缩包 zookeeper-3.4.14.tar.gz上传到linux系统/opt/lagou/software
解压 压缩包

tar -zxvf zookeeper-3.4.14.tar.gz -C ../servers/

修改配置文件创建data与log目录

#创建zk存储数据目录
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data
#创建zk日志文件目录
mkdir -p /opt/lagou/servers/zookeeper-3.4.14/data/logs
#修改zk配置文件
cd /opt/lagou/servers/zookeeper-3.4.14/conf
#文件改名
mv zoo_sample.cfg zoo.cfg
vim zoo.cfg
#更新datadir
dataDir=/opt/lagou/servers/zookeeper-3.4.14/data
#增加logdir
dataLogDir=/opt/lagou/servers/zookeeper-3.4.14/data/logs
#增加集群配置
##server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
server.1=linux121:2888:3888
server.2=linux122:2888:3888
server.3=linux123:2888:3888
#打开注释
#ZK提供了自动清理事务日志和快照文件的功能,这个参数指定了清理频率,单位是小时
autopurge.purgeInterval=1

 

添加myid配置
1. 在zookeeper的 data 目录下创建一个 myid 文件,内容为1,这个文件就是记录每个服务器的ID

cd /opt/lagou/servers/zookeeper-3.4.14/data
echo 1 > myid


安装包分发并修改myid的值

rsync -r /opt/lagou/servers/zookeeper-3.4.14 linux121:$PWD

修改myid值 linux122

echo 2 >/opt/lagou/servers/zookeeper-3.4.14/data/myid


修改myid值 linux123

echo 3 >/opt/lagou/servers/zookeeper-3.4.14/data/myid


依次启动三个zk实例

/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh start


启动命令(三个节点都要执行)

/opt/lagou/servers/zookeeper-3.4.14/bin/zkServer.sh status

查看zk启动情况
集群启动停止脚本 

#!/bin/sh
echo "start zookeeper server..."
if(($#==0));then
echo "no params";
exit;
fi
hosts="hadoop-1 hadoop-2 hadoop-3"
for host in $hosts
do
ssh $host "source /etc/profile; /opt/servers/zookeeper-3.4.14/bin/zkServer.sh $1"
done

3. Zookeeper数据结构与监听机制

ZooKeeper数据模型Znode
在ZooKeeper中,数据信息被保存在一个个数据节点上,这些节点被称为znode。ZNode 是Zookeeper 中最小数据单位,在 ZNode 下面又可以再挂 ZNode,这样一层层下去就形成了一个层次化命名空间 ZNode 树,我们称为 ZNode Tree,它采用了类似文件系统的层级树状结构进行管理。见下图


示例:
在 Zookeeper 中,每一个数据节点都是一个 ZNode,上图根目录下有两个节点,分别是:app1 和app2,其中 app1 下面又有三个子节点,所有ZNode按层次化进行组织,形成这么一颗树,ZNode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示,开发人员可以向这个节点写入数据,也可以在这个节点下面创建子节点。

3.1 ZNode 的类型

刚刚已经了解到,Zookeeper的znode tree是由一系列数据节点组成的,那接下来,我们就对数据节点做详细讲解
Zookeeper 节点类型可以分为三大类:

  • 持久性节点(Persistent)
  • 临时性节点(Ephemeral)
  • 顺序性节点(Sequential)

在开发中在创建节点的时候通过组合可以生成以下四种节点类型:持久节点、持久顺序节点、临时节点、临时顺序节点。不同类型的节点则会有不同的生命周期
持久节点:是Zookeeper中最常见的一种节点类型,所谓持久节点,就是指节点被创建后会一直存在服务器,直到删除操作主动清除

持久顺序节点:就是有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后面加上一个数字后缀,来表示其顺序。
临时节点:就是会被自动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建子节点。
临时顺序节点:就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。
事务ID
首先,先了解,事务是对物理和抽象的应用状态上的操作集合。往往在现在的概念中,狭义上的事务通常指的是数据库事务,一般包含了一系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
而在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新等操作。对于每一个事务请求,ZooKeeper都会为其分配一个全局唯一的事务ID,用 ZXID 来表示,通常是一个 64 位的数字。每一个 ZXID 对应一次
更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序zk中的事务指的是对zk服务器状态改变的操作(create,update data,更新字节点);zk对这些事务操作都会编号,这个编号是自增长的被称为ZXID。

3.2 ZNode 的状态信息

#使用bin/zkCli.sh 连接到zk集群
[zk: localhost:2181(CONNECTED) 2] get /zookeeper
cZxid = 0x0
ctime = Wed Dec 31 19:00:00 EST 1969
mZxid = 0x0
mtime = Wed Dec 31 19:00:00 EST 1969
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

整个 ZNode 节点内容包括两部分:节点数据内容和节点状态信息。数据内容是空,其他的属于状态信息。那么这些状态信息都有什么含义呢?

cZxid 就是 Create ZXID,表示节点被创建时的事务ID。
ctime 就是 Create Time,表示节点创建时间。
mZxid 就是 Modified ZXID,表示节点最后一次被修改时的事务ID。
mtime 就是 Modified Time,表示节点最后一次被修改的时间。
pZxid 表示该节点的子节点列表最后一次被修改时的事务 ID。只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新。
cversion 表示子节点的版本号。
dataVersion 表示内容版本号。
aclVersion 标识acl版本
ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0
dataLength 表示数据长度。
numChildren 表示直系子节点数。

3.3 Watcher 机制


Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能
一个典型的发布/订阅模型系统定义了一种 一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。
在 ZooKeeper 中,引入了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么Zk就会向指定客户端发送一个事件通知来实现分布式的通知功能。整个Watcher注册与通知过程如图所示。


Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。
具体工作流程为:

  • 客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中
  • 当Zookeeper服务器触发Watcher事件后,会向客户端发送通知
  • 客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑

4. Zookeeper的基本使用

4.1 ZooKeeper命令行操作

现在已经搭建起了一个能够正常运行的zookeeper服务了,所以接下来,就是来借助客户端来对zookeeper的数据节点进行操作
首先,进入到zookeeper的bin目录之后
通过zkClient进入zookeeper客户端命令行

./zkcli.sh 连接本地的zookeeper服务器
./zkCli.sh -server ip:port(2181) 连接指定的服务器

连接成功之后,系统会输出Zookeeper的相关环境及配置信息等信息。输入help之后,屏幕会输出可用的Zookeeper命令

[zk: localhost:2181(CONNECTED) 3] help
ZooKeeper -server host:port cmd args
stat path [watch]
set path data [version]
ls path [watch]
delquota [-n|-b] path
ls2 path [watch]
setAcl path acl
setquota -n|-b val path
history
redo cmdno
printwatches on|off
delete path [version]
sync path
listquota path
rmr path
get path [watch]
create [-s] [-e] path data acl
addauth scheme auth
quit
getAcl path
close
connect host:port

创建节点
使用create命令,可以创建一个Zookeeper节点, 如

create [-s][-e] path data
其中,-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点


① 创建顺序节点
使用 create -s /zk-test 123 命令创建zk-test顺序节点

[zk: localhost:2181(CONNECTED) 4] create -s /zk-test 123
Created /zk-test0000000000

执行完后,就在根节点下创建了一个叫做/zk-test的节点,该节点内容就是123,同时可以看到创建的zk-test节点后面添加了一串数字以示区别
② 创建临时节点
使用 create -e /zk-temp 123 命令创建zk-temp临时节

[zk: localhost:2181(CONNECTED) 1] create -e /zk-temp 123
Created /zk-temp
[zk: localhost:2181(CONNECTED) 2] ls /
[zk-test0000000000, zookeeper, zk-temp]

临时节点在客户端会话结束后,就会自动删除,下面使用quit命令退出客户端

[zk: localhost:2181(CONNECTED) 3] quit
Quitting...

再次使用客户端连接服务端,并使用ls / 命令查看根目录下的节点

[zk: localhost:2181(CONNECTED) 0] ls /
[zk-test0000000000, zookeeper]

可以看到根目录下已经不存在zk-temp临时节点了
③ 创建永久节点
使用 create /zk-permanent 123 命令创建zk-permanent永久节点 ,可以看到永久节点不同于顺序节点,不会自动在后面添加一串数字

[zk-test0000000000, zookeeper]
[zk: localhost:2181(CONNECTED) 1] create /zk-permanent 123
Created /zk-permanent
[zk: localhost:2181(CONNECTED) 2] ls /
[zk-permanent, zk-test0000000000, zookeeper]

读取节点
与读取相关的命令有ls 命令和get 命令
ls命令可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点;

ls path
其中,path表示的是指定数据节点的节点路径

get命令可以获取Zookeeper指定节点的数据内容和属性信息。

get path

若获取根节点下面的所有子节点,使用ls / 命令即可

[zk: localhost:2181(CONNECTED) 2] ls /
[zk-permanent, zk-test0000000000, zookeeper]

若想获取/zk-permanent的数据内容和属性,可使用如下命令:get /zk-permanent

[zk: localhost:2181(CONNECTED) 3] get /zk-permanent
123
cZxid = 0x300000008
ctime = Thu Jul 16 04:33:41 EDT 2020
mZxid = 0x300000008
mtime = Thu Jul 16 04:33:41 EDT 2020
pZxid = 0x300000008
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0

从上面的输出信息中,我们可以看到,第一行是节点/zk-permanent 的数据内容,其他几行则是创建该节点的事务ID(cZxid)、最后一次更新该节点的事务ID(mZxid)和最后一次更新该节点的时间mtime)等属性信息

更新节点
使用set命令,可以更新指定节点的数据内容,用法如下

set path data

其中,data就是要更新的新内容,version表示数据版本,在zookeeper中,节点的数据是有版本概念的,这个参数用于指定本次更新操作是基于Znode的哪一个数据版本进行的,如将/zk-permanent节点的数据更新为456,可以使用如下命令:set /zk-permanent 456
现在dataVersion已经变为1了,表示进行了更新

[zk: localhost:2181(CONNECTED) 4] set /zk-permanent 456
cZxid = 0x300000008
ctime = Thu Jul 16 04:33:41 EDT 2020
mZxid = 0x300000009
mtime = Thu Jul 16 05:07:00 EDT 2020
pZxid = 0x300000008
cversion = 0
dataVersion = 1
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 3
numChildren = 0

删除节点
使用delete命令可以删除Zookeeper上的指定节点,用法如下

delete path

其中version也是表示数据版本,使用delete /zk-permanent 命令即可删除/zk-permanent节点

[zk: localhost:2181(CONNECTED) 8] delete /zk-permanent
[zk: localhost:2181(CONNECTED) 9] ls /
[zk-test0000000000, zookeeper]

可以看到,已经成功删除/zk-permanent节点。值得注意的是,若删除节点存在子节点,那么无法删除该节点,必须先删除子节点,再删除父节点

4.2 Zookeeper-开源客户端

ZkClient
ZkClient是Github上一个开源的zookeeper客户端,在Zookeeper原生API接口之上进行了包装,是一个更易用的Zookeeper客户端,同时,zkClient在内部还实现了诸如Session超时重连、Watcher反复注册等功能接下来,还是从创建会话、创建节点、读取数据、更新数据、删除节点等方面来介绍如何使用zkClient这个zookeeper客户端
添加依赖:
在pom.xml文件中添加如下内容

<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.2</version>
</dependency>

1.创建会话
使用ZkClient可以轻松的创建会话,连接到服务端。
运行结果:ZooKeeper session created.
结果表明已经成功创建会话。
2. 创建节点
ZkClient提供了递归创建节点的接口,即其帮助开发者先完成父节点的创建,再创建子节点

运行结果:success create znode.
结果表明已经成功创建了节点,值得注意的是,ZkClient通过设置createParents参数为true可以递归的先创建父节点,再创建子节点
3.删除节点
ZkClient提供了递归删除节点的接口,即其帮助开发者先删除所有子节点(存在),再删除父节点。

import org.I0Itec.zkclient.ZkClient;

public class ZkDemo {
    public static void main(String[] args) {
        //先获取到zkclient对象,client与zk集群通信端口是2181
        final ZkClient zkClient = new ZkClient("linux121:2181"); //建立了到zk集群的会话
        System.out.println("zkclient is ready");
        //1 创建节点
        zkClient.createPersistent("/la-client/lg-c1", true); //如果需要级联创建,第二个参数设置为true
        System.out.println("path is created");

        //2删除节点
//        zkClient.delete("/la-client");
        zkClient.deleteRecursive("/la-client");//递归删除可以删除非空节点,先删除子节点然后删除父节点
        System.out.println("delete path is success");
    }
}

运行结果: success delete znode.
结果表明ZkClient可直接删除带子节点的父节点,因为其底层先删除其所有子节点,然后再删除父节点
4. 监听节点变化

import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.client.ZooKeeperSaslClient;

import java.util.List;

/*
演示zkClient如何使用监听器
 */
public class Get_Child_Change {
    public static void main(String[] args) throws InterruptedException {
        //获取到zkClient
        final ZkClient zkClient = new ZkClient("linux121:2181");

        //zkClient对指定目录进行监听(不存在目录:/lg-client),指定收到通知之后的逻辑

        //对/lag-client注册了监听器,监听器是一直监听
        zkClient.subscribeChildChanges("/lg-client", new IZkChildListener() {
            //该方法是接收到通知之后的执行逻辑定义
            public void handleChildChange(String path, List<String> childs) throws Exception {
                //打印节点信息
                System.out.println(path + " childs changes ,current childs " + childs);
            }
        });

        //使用zkClient创建节点,删除节点,验证监听器是否运行
        zkClient.createPersistent("/lg-client");
        Thread.sleep(1000); //只是为了方便观察结果数据
        zkClient.createPersistent("/lg-client/c1");
        Thread.sleep(1000);
        zkClient.delete("/lg-client/c1");
        Thread.sleep(1000);

        zkClient.delete("/lg-client");

        Thread.sleep(Integer.MAX_VALUE);

        /*
        1 监听器可以对不存在的目录进行监听
        2 监听目录下子节点发生改变,可以接收到通知,携带数据有子节点列表
        3 监听目录创建和删除本身也会被监听到
         */
    }
}

运行结果:

/lg-zkClient 's child changed, currentChilds:[]
/lg-zkClient 's child changed, currentChilds:[c1]
/lg-zkClient 's child changed, currentChilds:[]
/lg-zkClient 's child changed, currentChilds:null

结果表明:
客户端可以对一个不存在的节点进行子节点变更的监听。
一旦客户端对一个节点注册了子节点列表变更监听之后,那么当该节点的子节点列表发生变更时,服务端都会通知客户端,并将最新的子节点列表发送给客户端
该节点本身的创建或删除也会通知到客户端。
5. 获取数据(节点是否存在、更新、删除)

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;

//使用监听器监听节点数据的变化
public class Get_Data_Change {

    public static void main(String[] args) throws InterruptedException {
        // 获取zkClient对象
        final ZkClient zkClient = new ZkClient("linux121:2181");
        //设置自定义的序列化类型,否则会报错!!
        zkClient.setZkSerializer(new ZkStrSerializer());


        //判断节点是否存在,不存在创建节点并赋值
        final boolean exists = zkClient.exists("/lg-client1");
        if (!exists) {
            zkClient.createEphemeral("/lg-client1", "123");
        }

        //注册监听器,节点数据改变的类型,接收通知后的处理逻辑定义
        zkClient.subscribeDataChanges("/lg-client1", new IZkDataListener() {
            public void handleDataChange(String path, Object data) throws Exception {
                //定义接收通知之后的处理逻辑
                System.out.println(path + " data is changed ,new data " + data);
            }

            //数据删除--》节点删除
            public void handleDataDeleted(String path) throws Exception {
                System.out.println(path + " is deleted!!");
            }
        });

        //更新节点的数据,删除节点,验证监听器是否正常运行
        final Object o = zkClient.readData("/lg-client1");
        System.out.println(o);

        zkClient.writeData("/lg-client1", "new data");
        Thread.sleep(1000);

        //删除节点
        zkClient.delete("/lg-client1");
        Thread.sleep(Integer.MAX_VALUE);
    }
}
import org.I0Itec.zkclient.exception.ZkMarshallingError;
import org.I0Itec.zkclient.serialize.ZkSerializer;

public class ZkStrSerializer implements ZkSerializer {

    //序列化,数据--》byte[]
    public byte[] serialize(Object o) throws ZkMarshallingError {
        return String.valueOf(o).getBytes();
    }

    //反序列化,byte[]--->数据
    public Object deserialize(byte[] bytes) throws ZkMarshallingError {
        return new String(bytes);
    }
}

运行结果:

123
/lg-client1 data is changed ,new data new data
/lg-client1 is deleted!!

结果表明可以成功监听节点数据变化或删除事件。


5. Zookeeper内部原理

5.1 Leader选举

选举机制

  • 半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器。
  • Zookeeper虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper工作时,是有一个节点为Leader,其它为Follower,Leader是通过内部的选举机制产生的。

集群首次启动
假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动,来看看会发生什么,


Zookeeper的选举机制
(1)服务器1启动,此时只有它一台服务器启动了,它发出去的报文没有任何响应,所以它的选举状态一直是LOOKING状态。
(2)服务器2启动,它与最开始启动的服务器1进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以id值较大的服务器2胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是3),所以服务器1、2还是继续保持LOOKING状态。
(3)服务器3启动,根据前面的理论分析,服务器3成为服务器1、2、3中的老大,而与上面不同的是,此时有三台服务器选举了它,所以它成为了这次选举的Leader。
(4)服务器4启动,根据前面的分析,理论上服务器4应该是服务器1、2、3、4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,所以它只能接收当小弟的命了。
(5)服务器5启动,同4一样称为follower。


集群非首次启动

每个节点在选举时都会参考自身节点的zxid值(事务ID);优先选择zxid值大的节点称为Leader!!

5.2 ZAB一致性协议

1. 分布式数据一致性问题

为什么会出现分布式数据一致性问题?

  • 将数据复制到分布式部署的多台机器中,可以消除单点故障,防止系统由于某台(些)机器宕机导致的不可用。
  • 通过负载均衡技术,能够让分布在不同地方的数据副本全都对外提供服务。有效提高系统性能。

在分布式系统中引入数据复制机制后,多台数据节点之间由于网络等原因很容易产生数据不一致的情况。
举例
当客户端Client1将系统中的一个值K1由V1更新为V2,但是客户端Client2读取的是一个还没有同步更新的副本,K1的值依然是V1,这就导致了数据的不一致性。其中,常见的就是主从数据库之间的复制延时问题。


2. ZAB协议
ZK就是分布式一致性问题的工业解决方案,paxos是其底层理论算法(晦涩难懂著名),其中zab,raft和众多开源算法是对paxos的工业级实现。ZK没有完全采用paxos算法,而是使用了一种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)的协议作为其数据一致性的核心算法。
ZAB协议
ZAB 协议是为分布式协调服务 Zookeeper 专门设计的一种支持崩溃恢复和原子广播协议。
主备模式保证一致性

ZK怎么处理集群中的数据?所有客户端写入数据都是写入Leader中,然后,由 Leader 复制到Follower中。ZAB会将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程上,ZAB协议能够保证了事务操作的一个全局的变更序号(ZXID)。
广播消息
ZAB 协议的消息广播过程类似于 二阶段提交过程。对于客户端发送的写请求,全部由 Leader 接收,Leader 将请求封装成一个事务 Proposal(提议),将其发送给所有 Follwer ,如果收到超过半数反馈ACK,则执行 Commit 操作(先提交自己,再发送 Commit 给所有 Follwer)。
1. 发送Proposal到Follower

2. Leader接收Follower的ACK

不能正常反馈Follower恢复正常后会进入数据同步阶段最终与Leader保持一致!!
细节

  • 内保证所有Leader接收到Client请求之后,会将这个请求封装成一个事务,并给这个事务分配一个全局递增的唯一 ID,称为事务ID(ZXID),ZAB 协议要求保证事务的顺序,因此必须将每一个事务按照 ZXID进行先后排序然后处理。
  • ZK集群为了保证任何事务操作能够有序的顺序执行,只能是 Leader 服务器接受写请求,即使是Follower 服务器接受到客户端的请求,也会转发到 Leader 服务器进行处理。

zk提供的应该是最终一致性的标准。zk所有节点接收写请求之后可以在一定时间节点都能看到该条数据!!
Leader 崩溃问题
Leader宕机后,ZK集群无法正常工作,ZAB协议提供了一个高效且可靠的leader选举算法。
Leader宕机后,被选举的新Leader需要解决的问题

  • ZAB 协议确保那些已经在 Leader 提交的事务最终会被所有服务器提交。
  • ZAB 协议确保丢弃那些只在 Leader 提出/复制,但没有提交的事务。

基于上面的目的,ZAB协议设计了一个选举算法:能够确保已经被Leader提交的事务被集群接受,丢弃还没有提交的事务。
这个选举算法的关键点:保证选举出的新Leader拥有集群中所有节点最大编号(ZXID)的事务!!

6. Zookeeper应用实践

ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使用它来进行分布式数据的发布与订阅。另一方面,通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能,如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式
应用场景来做下介绍
Zookeeper的两大特性:

  • 1.客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
  • 2.对在Zookeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除

利用其两大特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建一个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。

6.1 服务器动态上下线监听

分布式系统中,主节点会有多台,主节点可能因为任何原因出现宕机或者下线,而任意一台客户端都要能实时感知到主节点服务器的上下线。
思路分析


具体实现
服务端

import org.I0Itec.zkclient.ZkClient;

//服务端主要提供了client需要的一个时间查询服务,服务端向zk建立临时节点
public class Server {

    //获取zkclient
    ZkClient zkClient = null;

    private void connectZk() {
        // 创建zkclient
        zkClient = new ZkClient("linux121:2181,linux122:2181");
        //创建服务端建立临时节点的目录
        if (!zkClient.exists("/servers")) {
            zkClient.createPersistent("/servers");
        }
    }

    //告知zk服务器相关信息
    private void saveServerInfo(String ip, String port) {
        final String sequencePath = zkClient.createEphemeralSequential("/servers/server", ip + ":" + port);
        System.out.println("----->>> ,服务器:" + ip + ":" + port + ",向zk保存信息成功,成功上线可以接受client查询");
    }

    public static void main(String[] args) {
        //准备两个服务端启动上线(多线程模拟,一个线程代表一个服务器)
        final Server server = new Server();
        server.connectZk();
        server.saveServerInfo(args[0], args[1]);
        //提供时间服务的线程没有启动,创建一个线程类,可以接收socket请求
        new TimeService(Integer.parseInt(args[1])).start();
    }
}

服务端提供时间查询的线程类

import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;

//提供时间查询服务
public class TimeService extends Thread {
    private int port = 0;

    public TimeService(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        //通过socket与client进行交流,启动serversocket监听请求
        try {
            //指定监听的端口
            final ServerSocket serverSocket = new ServerSocket(port);

            //保证服务端一直运行
            while (true) {
                final Socket socket = serverSocket.accept();
                //不关心client发送内容,server只考虑发送一个时间值
                final OutputStream out = socket.getOutputStream();
                out.write(new Date().toString().getBytes());
            }

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

client端

import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

// 注册监听zk指定目录,
//维护自己本地一个servers信息,收到通知要进行更新
//发送时间查询请求并接受服务端返回的数据
public class Client {
    //获取zkclient
    ZkClient zkClient = null;

    //维护一个serversi 信息集合
    ArrayList<String> infos = new ArrayList<String>();

    private void connectZk() {
        // 创建zkclient
        zkClient = new ZkClient("linux121:2181,linux122:2181");
        //第一次获取服务器信息,所有的子节点
        final List<String> childs = zkClient.getChildren("/servers");
        for (String child : childs) {
            //存储着ip+port
            final Object o = zkClient.readData("/servers/" + child);
            infos.add(String.valueOf(o));
        }

        //对servers目录进行监听
        zkClient.subscribeChildChanges("/servers", new IZkChildListener() {
            public void handleChildChange(String s, List<String> children) throws Exception {
                //接收到通知,说明节点发生了变化,client需要更新infos集合中的数据
                ArrayList<String> list = new ArrayList<String>();
                //遍历更新过后的所有节点信息
                for (String path : children) {
                    final Object o = zkClient.readData("/servers/" + path);
                    list.add(String.valueOf(o));
                }

                //最新数据覆盖老数据
                infos = list;
                System.out.println("--》接收到通知,最新服务器信息为:" + infos);
            }
        });
    }


    //发送时间查询的请求
    public void sendRequest() throws IOException {
        //目标服务器地址
        final Random random = new Random();
        final int i = random.nextInt(infos.size());
        final String ipPort = infos.get(i);
        final String[] arr = ipPort.split(":");

        //建立socket连接

        final Socket socket = new Socket(arr[0], Integer.parseInt(arr[1]));
        final OutputStream out = socket.getOutputStream();
        final InputStream in = socket.getInputStream();
        //发送数据
        out.write("query time".getBytes());
        out.flush();
        //接收返回结果
        final byte[] b = new byte[1024];
        in.read(b);//读取服务端返回数据
        System.out.println("client端接收到server:+" + ipPort + "+返回结果:" + new String(b));


        //释放资源
        in.close();
        out.close();
        socket.close();
    }


    public static void main(String[] args) throws InterruptedException {

        final Client client = new Client();
        client.connectZk();   //监听器逻辑
        while (true) {
            try {
                client.sendRequest(); //发送请求
            } catch (IOException e) {
                e.printStackTrace();
                try {
                    client.sendRequest();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            //每隔几秒中发送一次请求到服务端
            Thread.sleep(2000);
        }
    }
}

6.2 分布式锁

1. 什么是锁
在单机程序中,当存在多个线程可以同时改变某个变量(可变共享变量)时,为了保证线程安全(数据不能出现脏数据)就需要对变量或代码块做同步,使其在修改这种变量时能够串行执行消除并发修改变量。
对变量或者堆代码码块做同步本质上就是加锁。目的就是实现多个线程在一个时刻同一个代码块只能有一个线程可执行
2. 分布式锁
分布式的环境中会不会出现脏数据的情况呢?类似单机程序中线程安全的问题。观察下面的例子


上面的设计是存在线程安全问题
问题
假设Redis 里面的某个商品库存为 1;此时两个用户同时下单,其中一个下单请求执行到第 3 步,更新数据库的库存为 0,但是第 4 步还没有执行。
而另外一个用户下单执行到了第 2 步,发现库存还是 1,就继续执行第 3 步。但是商品库存已经为0,所以如果数据库没有限制就会出现超卖的问题。
解决方法
用锁把 2、3、4 步锁住,让他们执行完之后,另一个线程才能进来执行。

公司业务发展迅速,系统应对并发不断提高,解决方案是要增加一台机器,结果会出现更大的问题


假设有两个下单请求同时到来,分别由两个机器执行,那么这两个请求是可以同时执行了,依然存在超卖的问题。
因为如图所示系统是运行在两个不同的 JVM 里面,不同的机器上,增加的锁只对自己当前 JVM 里面的线程有效,对于其他 JVM 的线程是无效的。所以现在已经不是线程安全问题。需要保证两台机器加的锁是同一个锁,此时分布式锁就能解决该问题。
分布式锁的作用:在整个系统提供一个全局、唯一的锁,在分布式系统中每个系统在进行相关操作的时候需要获取到该锁,才能执行相应操作。
3 zk实现分布式锁
利用Zookeeper可以创建临时带序号节点的特性来实现一个分布式锁
实现思路

  • 锁就是zk指定目录下序号最小的临时序列节点,多个系统的多个线程都要在此目录下创建临时的顺序节点,因为Zk会为我们保证节点的顺序性,所以可以利用节点的顺序进行锁的判断。
  • 每个线程都是先创建临时顺序节点,然后获取当前目录下最小的节点(序号),判断最小节点是不是当前节点,如果是那么获取锁成功,如果不是那么获取锁失败。
  • 获取锁失败的线程获取当前节点上一个临时顺序节点,并对对此节点进行监听,当该节点删除的时候(上一个线程执行结束删除或者是掉线zk删除临时节点)这个线程会获取到通知,代表获取到了锁。

流程图


main方法

//zk实现分布式锁
public class DisLockTest {
    public static void main(String[] args) {
        //使用10个线程模拟分布式环境
        for (int i = 0; i < 10; i++) {
            new Thread(new DisLockRunnable()).start();//启动线程
        }
    }

    static class DisLockRunnable implements Runnable {

        public void run() {
            //每个线程具体的任务,每个线程就是抢锁,
            final DisClient client = new DisClient();
            client.getDisLock();

            //模拟获取锁之后的其它动作
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //释放锁
            client.deleteLock();
        }
    }
}

 

分布式锁的实现可以是 Redis、Zookeeper,相对来说生产环境如果使用分布式锁可以考虑使用Redis实现而非Zk。

7 Hadoop HA

7.1 HA 概述

1. 所谓HA(High Available),即高可用(7*24小时不中断服务)。
2. 实现高可用最关键的策略是消除单点故障。Hadoop-HA严格来说应该分成各个组件的HA机制:HDFS的HA和YARN的HA。
3. Hadoop2.0之前,在HDFS集群中NameNode存在单点故障(SPOF)。
4. NameNode主要在以下两个方面影响HDFS集群
NameNode机器发生意外,如宕机,集群将无法使用,直到管理员重启
NameNode机器需要升级,包括软件、硬件升级,此时集群也将无法使用

HDFS HA功能通过配置Active/Standby两个NameNodes实现在集群中对NameNode的热备来解决上述
问题。如果出现故障,如机器崩溃或机器需要升级维护,这时可通过此种方式将NameNode很快的切换到另外一台机器。

7.2 HDFS-HA 工作机制

通过双NameNode消除单点故障(Active/Standby)
2.1 HDFS-HA工作要点
1. 元数据管理方式需要改变

  • 内存中各自保存一份元数据;
  • Edits日志只有Active状态的NameNode节点可以做写操作;
  • 两个NameNode都可以读取Edits;
  • 共享的Edits放在一个共享存储中管理(qjournal和NFS两个主流实现);

2. 需要一个状态管理功能模块

  • 实现了一个zkfailover,常驻在每一个namenode所在的节点,每一个zkfailover负责监控自己所在
  • NameNode节点,利用zk进行状态标识,当需要进行状态切换时,由zkfailover来负责切换,切换
  • 时需要防止brain split现象的发生(集群中出现两个Active的Namenode)。

3. 必须保证两个NameNode之间能够ssh无密码登录
4. 隔离(Fence),即同一时刻仅仅有一个NameNode对外提供服务

2.2 HDFS-HA工作机制

配置部署HDFS-HA进行自动故障转移。自动故障转移为HDFS部署增加了两个新组件:ZooKeeper和ZKFailoverController(ZKFC)进程,ZooKeeper是维护少量协调数据,通知客户端这些数据的改变和监视客户端故障的高可用服务。HA的自动故障转移依赖于ZooKeeper的以下功能:
故障检测

  • 集群中的每个NameNode在ZooKeeper中维护了一个临时会话,如果机器崩溃,ZooKeeper中的会话将终止,ZooKeeper通知另一个NameNode需要触发故障转移。

现役NameNode选择

  • ZooKeeper提供了一个简单的机制用于唯一的选择一个节点为active状态。如果目前现役NameNode崩溃,另一个节点可能从ZooKeeper获得特殊的排外锁以表明它应该成为现役NameNode。

ZKFC是自动故障转移中的另一个新组件,是ZooKeeper的客户端,也监视和管理NameNode的状态。每个运行NameNode的主机也运行了一个ZKFC进程,ZKFC负责:

健康监测

  • ZKFC使用一个健康检查命令定期地ping与之在相同主机的NameNode,只要该NameNode及时地回复健康状态,ZKFC认为该节点是健康的。如果该节点崩溃,冻结或进入不健康状态,健康监测器标识该节点为非健康的。

ZooKeeper会话管理

  • 当本地NameNode是健康的,ZKFC保持一个在ZooKeeper中打开的会话。如果本地NameNode处于active状态,ZKFC也保持一个特殊的znode锁,该锁使用了ZooKeeper对短暂节点的支持,如果会话终止,锁节点将自动删除。

基于ZooKeeper的选择

  • 如果本地NameNode是健康的,且ZKFC发现没有其它的节点当前持有znode锁,它将为自己获取该锁。如果成功,则它已经赢得了选择,并负责运行故障转移进程以使它的本地NameNode为Active。故障转移进程与前面描述的手动故障转移相似,首先如果必要保护之前的现役NameNode,然后本地NameNode转换为Active状态。

 7.4 YARN-HA配置

4.1 YARN-HA工作机制
1. 官方文档

https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarnsite/ResourceManagerHA.html

2. YARN-HA工作机制,如图

 

第一部分 初识 HBase

第 1 节 HBase 简介

1.1 HBase是什什么
HBase 基于 Google的BigTable论文而来,是一个分布式海量列式非关系型数据库系统,可以提供超大规模数据集的实时随机读写。
接下来,通过一个场景初步认识HBase列列存储
如下MySQL存储机制,浪费存储空间

空值字段列列存储的优点:
1)减少存储空间占⽤用。
2)⽀支持好多列列
1.2 HBase的特点
海量存储: 底层基于HDFS存储海量数据
列式存储:HBase表的数据是基于列族进行存储的,一个列族包含若干列
极易扩展:底层依赖HDFS,当磁盘空间不足的时候,只需要动态增加DataNode服务节点就可以
高并发:支持高并发的读写请求
稀疏:稀疏主要是针对HBase列的灵活性,在列族中,你可以指定任意多的列,在列数据为空的情况下,是不会占用存储空间的。
数据的多版本:HBase表中的数据可以有多个版本值,默认情况下是根据版本号去区分,版本号就是插入数据的时间戳
数据类型单一:所有的数据在HBase中是以字节数组进行存储

第 2 节 HBase数据模型

HBase的数据也是以表(有行有列)的形式存储
HBase逻辑架构


HBase物理理存储

第 3 节 HBase整体架构

Zookeeper

  • 实现了HMaster的高可用
  • 保存了HBase的元数据信息,是所有HBase表的寻址入口
  • 对HMaster和HRegionServer实现了监控

HMaster(Master)

  • 为HRegionServer分配Region
  • 维护整个集群的负载均衡
  • 维护集群的元数据信息
  • 发现失效的Region,并将失效的Region分配到正常的HRegionServer上

HRegionServer(RegionServer)

  • 负责管理理Region
  • 接受客户端的读写数据请求
  • 切分在运⾏行行过程中变⼤大的Region

Region

  • 每个HRegion由多个Store构成,
  • 每个Store保存一个列列族(Columns Family),表有几个列列族,则有几个Store,
  • 每个Store由一个MemStore和多个StoreFile组成,MemStore是Store在内存中的内容,写到文件后就是StoreFile。StoreFile底层是以HFile的格式保存。

第 4 节 HBase集群安装部署

(1)下载安装包
http://archive.apache.org/dist/hbase/1.3.1/
hbase-1.3.1-bin.tar.gz
(2)规划安装目录

  • /opt/lagou/servers/

(3)上传安装包到服务器
(4)解压安装包到指定的规划目录

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

(5)修改配置⽂文件
需要把hadoop中的配置core-site.xml 、hdfs-site.xml拷贝到hbase安装⽬目录下的conf文件夹中

ln -s /opt/lagou/servers/hadoop-2.9.2/etc/hadoop/core-site.xml /opt/lagou/servers/hbase-1.3.1/conf/core-site.xml
ln -s /opt/lagou/servers/hadoop-2.9.2/etc/hadoop/hdfs-site.xml /opt/lagou/servers/hbase-1.3.1/conf/hdfs-site.xml

修改con目录下配置文件
修改 hbase-env.sh

#添加java环境变量量
export JAVA_HOME=/opt/module/jdk1.8.0_231
#指定使⽤用外部的zk集群
export HBASE_MANAGES_ZK=FALSE


修改 hbase-site.xml

<configuration>
<!-- 指定hbase在HDFS上存储的路路径 -->
<property>
<name>hbase.rootdir</name>
<value>hdfs://linux121:9000/hbase</value>
</property>
<!-- 指定hbase是分布式的 -->
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<!-- 指定zk的地址,多个⽤用“,”分割 -->
<property>
<name>hbase.zookeeper.quorum</name>
<value>linux121:2181,linux122:2181,linux123:2181</value>
</property>
</configuration>

修改regionservers文件

#指定regionserver节点
linux121
linux122
linux123

hbase的conf目录下创建文件backup-masters (Standby Master)

linux122

(6)配置hbase的环境变量

export HBASE_HOME=/opt/lagou/servers/hbase-1.3.1
export PATH=$PATH:$HBASE_HOME/bin

(7)分发hbase⽬目录和环境变量量到其他节点

rsync-script hbase-1.3.1

(8)让所有节点的hbase环境变量生效
在所有节点执⾏行行 source /etc/profile
HBase集群的启动和停止

  • 前提条件:先启动hadoop和zk集群
  • 启动HBase:start-hbase.sh
  • 停止HBase:stop-hbase.sh

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


第 5 节 HBase shell 基本操作


1、进⼊入Hbase客户端命令操作界面

hbase shell

2、查看帮助命令

hbase(main):001:0> help

3、查看当前数据库中有哪些表

hbase(main):006:0> list

4、创建一张lagou表, 包含base_info、extra_info两个列族

hbase(main):001:0> create 'lagou', 'base_info', 'extra_info'
或者(Hbase建表必须指定列列族信息)
create 'lagou', {NAME => 'base_info', VERSIONS => '3'},{NAME => 'extra_info',VERSIONS => '3'}
VERSIONS 是指此单元格内的数据可以保留留最近的 3 个版本

5、添加数据操作
向lagou表中插⼊入信息,row key为 rk1,列列族base_info中添加name列列标示符,值为wang

hbase(main):001:0> put 'lagou', 'rk1', 'base_info:name', 'wang'

向lagou表中插⼊入信息,row key为rk1,列列族base_info中添加age列列标示符,值为30

hbase(main):001:0> put 'lagou', 'rk1', 'base_info:age', 30

向lagou表中插⼊入信息,row key为rk1,列列族extra_info中添加address列列标示符,值为shanghai

hbase(main):001:0> put 'lagou', 'rk1', 'extra_info:address', 'shanghai'


6、查询数据
6.1 通过rowkey进⾏行行查询
获取表中row key为rk1的所有信息

hbase(main):001:0> get 'lagou', 'rk1'

6.2 查看rowkey下⾯面的某个列列族的信息
获取lagou表中row key为rk1,base_info列列族的所有信息

hbase(main):001:0> get 'lagou', 'rk1', 'base_info'

6.3 查看rowkey指定列列族指定字段的值

  • 获取表中row key为rk1,base_info列列族的name、age列列标示符的信息
hbase(main):008:0> get 'lagou', 'rk1', 'base_info:name', 'base_info:age'

6.4 查看rowkey指定多个列列族的信息
获取lagou表中row key为rk1,base_info、extra_info列列族的信息

hbase(main):010:0> get 'lagou', 'rk1', 'base_info', 'extra_info'
或者
hbase(main):011:0> get 'lagou', 'rk1', {COLUMN => ['base_info', 'extra_info']}
或者
hbase(main):012:0> get 'lagou', 'rk1', {COLUMN => ['base_info:name', 'extra_info:address']}


6.5 指定rowkey与列列值查询
获取表中row key为rk1,cell的值为wang的信息

hbase(main):001:0> get 'lagou', 'rk1', {FILTER => "ValueFilter(=, 'binary:wang')"}

6.6 指定rowkey与列列值模糊查询
获取表中row key为rk1,列列标示符中含有a的信息

hbase(main):001:0> get 'lagou', 'rk1', {FILTER => "(QualifierFilter(=,'substring:a'))"}

6.7 查询所有数据
查询lagou表中的所有信息

hbase(main):000:0> scan 'lagou'

6.8 列列族查询
查询表中列列族为 base_info 的信息

hbase(main):001:0> scan 'lagou', {COLUMNS => 'base_info'}
hbase(main):002:0> scan 'lagou', {COLUMNS => 'base_info', RAW => true, VERSIONS => 3}
## Scan时可以设置是否开启Raw模式,开启Raw模式会返回包括已添加删除标记但是未实际删除的数据
## VERSIONS指定查询的最⼤大版本数

6.9 指定多个列列族与按照数据值模糊查询
查询lagou表中列列族为 base_info 和 extra_info且列列标示符中含有a字符的信息

hbase(main):001:0> scan 'lagou', {COLUMNS => ['base_info', 'extra_info'], FILTER => "(QualifierFilter(=,'substring:a'))"}

6.10 rowkey的范围值查询(⾮非常重要)
查询lagou表中列列族为base_info,rk范围是[rk1, rk3)的数据(rowkey底层存储是字典序)按rowkey顺序存储。

hbase(main):001:0> scan 'lagou', {COLUMNS => 'base_info', STARTROW => 'rk1', ENDROW => 'rk3'}

6.11 指定rowkey模糊查询
查询lagou表中row key以rk字符开头的

hbase(main):001:0> scan 'lagou',{FILTER=>"PrefixFilter('rk')"}

7、更更新数据
更新操作同插入操作一模一样,只不过有数据就更新,没数据就添加

7.1 更更新数据值
把lagou表中rowkey为rk1的base_info列列族下的列name修改为liang

hbase(main):030:0> put 'lagou', 'rk1', 'base_info:name', 'liang'

8、删除数据和表
8.1 指定rowkey以及列名进行删除
删除lagou表row key为rk1,列列标示符为 base_info:name 的数据

hbase(main):002:0> delete 'lagou', 'rk1', 'base_info:name'

8.2 指定rowkey,列名以及字段值进行删除
删除lagou表row key为rk1,列列标示符为base_info:name的数据

hbase(main):033:0> delete 'lagou', 'rk1', 'base_info:age'

8.3 删除列列族
删除 base_info 列族

hbase(main):035:0> alter 'lagou', 'delete' => 'base_info'

8.4 清空表数据
删除lagou表数据

hbase(main):001:0> truncate 'lagou'

8.5 删除表
删除lagou表

#先disable 再drop
hbase(main):036:0> disable 'lagou'
hbase(main):037:0> drop 'lagou'
#如果不不进⾏行行disable,直接drop会报错
ERROR: Table user is enabled. Disable it first.

第二部分 HBase原理理深入

第 1 节 HBase读数据流程

HBase读操作

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

HBase上Regionserver的内存分为两个部分

  • 一部分作为Memstore,主要用来写;
  • 另外一部分作为BlockCache,主要用于读数据;

6)如果BlockCache中也没有找到,再到StoreFile上进行行读取

  • 从storeFile中读取到数据之后,不是直接把结果数据返回给客户端, 而是把数据先写入到BlockCache中,目的是为了加快后续的查询;然后在返回结果给客户端。

第 2 节 HBase写数据流程

HBase写操作

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

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

Flush机制

  • (1)当memstore的大小超过这个值的时候,会flush到磁盘,默认为128M
  • (2)当memstore中的数据时间超过1小时,会flush到磁盘
  • (3)HregionServer的全局memstore的大小,超过该大小会触发flush到磁盘的操作,默认是堆大小的40%
  • (4)手动flush

阻塞机制
以上介绍的是Store中memstore数据刷写磁盘的标准,但是Hbase中是周期性的检查是否满足以上标准满足则进行刷写,但是如果在下次检查到来之前,数据疯狂写
入Memstore中,会出现什什么问题呢?
会触发阻塞机制,此时无法写入数据到Memstore,数据无法写入Hbase集群。
memstore中数据达到512MB

  • 计算公式:hbase.hregion.memstore.flush.size*hbase.hregion.memstore..block.multiplier
  • hbase.hregion.memstore.flush.size刷写的阀值,默认是 134217728,即128MB。
  • hbase.hregion.memstore.block.multiplier是一个倍数,默认是4。

RegionServer全部memstore达到规定值

  • hbase.regionserver.global.memstore.size.lower.limit是0.95,
  • hbase.regionserver.global.memstore.size是0.4,
  • 堆内存总共是 16G,
  • 触发刷写的阈值是:6.08GB 触发阻塞的阈值是:6.4GB

Compact合并机制
在hbase中主要存在两种类型的compac合并
minor compact 小合并
在将Store中多个HFile(StoreFile)合并为一个HFile

  • 这个过程中,删除和更新的数据仅只是做了标记,并没有物理理移除,这种合并的触发频率很高。

minor compact文件选择标准由以下几个参数共同决定:

触发条件
memstore flush

  • 在进行memstore flush前后都会进行判断是否触发compact

定期检查线程

  • 周期性检查是否需要进行compaction操作,由参数:hbase.server.thread.wakefrequency决定,默认值是10000 millseconds

major compact 大合并
合并Store中所有的HFile为一个HFile
这个过程有删除标记的数据会被真正移除,同时超过单元格maxVersion的版本记录也会被删除。合并频率比较低,默认7天执行一次,并且性能消耗非常大,建议生产关闭(设置为0),在应用空闲时间手动触发。一般可以是手动控制进行行合并,防止出现在业务高峰期。

  • major compaction触发时间条件
  • 手动触发

第 4 节 Region 拆分机制

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

 4.1 拆分策略略

HBase的Region Split策略略一共有以下几种:
1)ConstantSizeRegionSplitPolicy
0.94版本前默认切分策略

  • 当region大小大于某个阈值(hbase.hregion.max.filesize=10G)之后就会触发切分,一个region等分为2个region。
  • 但是在生产线上这种切分策略略却有相当大的弊端:切分策略略对于大表和小表没有明显的区分。阈值(hbase.hregion.max.filesize)设置较大对大表比较友好,但是小表就有可能不会触发分裂,极端情况下可能就1个,这对业务来说并不是什么好事。如果设置较小则对小表友好,但一个大表就会在整个集群产生大量的region,这对于集群的管理理、资源使⽤用、failover来说都不不是一件好事。

2)IncreasingToUpperBoundRegionSplitPolicy
0.94版本~2.0版本默认切分策略

  • 切分策略略稍微有点复杂,总体看和ConstantSizeRegionSplitPolicy思路路相同,一个region大小大于设置阈值就会触发切分。但是这个阈值并不像ConstantSizeRegionSplitPolicy是一个固定的值,而是会在一定条件下不断调整,调整规则和region所属表在当前regionserver上的region个数有关系.
  • region split的计算公式是:
  • regioncount^3 * 128M * 2,当region达到该size的时候进行split
  • 例如:
  • 第一次split:1^3 * 256 = 256MB
  • 第二次split:2^3 * 256 = 2048MB
  • 第三次split:3^3 * 256 = 6912MB
  • 第四次split:4^3 * 256 = 16384MB > 10GB,因此取较小的值10GB
  • 后面每次split的size都是10GB了了

3)SteppingSplitPolicy
2.0版本默认切分策略略

  • 这种切分策略略的切分阈值又发生了变化,相比 IncreasingToUpperBoundRegionSplitPolicy 简单了一些,依然和待分裂region所属表在当前regionserver上的region个数有关系,如果region个数等于1,切分阈值为flush size * 2,否则为MaxRegionFileSize。这种切分策略略对于大集群中的大表、小表会IncreasingToUpperBoundRegionSplitPolicy 更加友好,小表不不会再产生大量的小region,而是适可而止。

4)KeyPrefixRegionSplitPolicy

  • 根据rowKey的前缀对数据进行分组,这里里是指定rowKey的前多少位作为前缀,比如rowKey都是16位的,指定前5位是前缀,那么前5位相同的rowKey在进行region split的时候会分到相同的region中。

5)DelimitedKeyPrefixRegionSplitPolicy

  • 保证相同前缀的数据在同一个region中,例例如rowKey的格式为:userid_eventtype_eventid,指定的delimiter为 _ ,则split的的时候会确保userid相同的数据在同一个region中。

6)DisabledRegionSplitPolicy

  • 不启用自动拆分, 需要指定手动拆分

4.2 RegionSplitPolicy的应用

Region拆分策略略可以全局统一配置,也可以为单独的表指定拆分策略。

  • 1)通过hbase-site.xml全局统一配置(对hbase所有表生效)
  • 2)通过Java API为单独的表指定Region拆分策略
  • 3)通过HBase Shell为单个表指定Region拆分策略

第 5 节 HBase表的预分区(region)

5.1 为何要预分区?

当一个table刚被创建的时候,Hbase默认的分配一个region给table。也就是说这个时候,所有的读写请求都会访问到同一个regionServer的同一个region中,这个时
候就达不到负载均衡的效果了,集群中的其他regionServer就可能会处于比较空闲的状态。解决这个问题可以用pre-splitting,在创建table的时候就配置好,生成多个
region。

  • 增加数据读写效率
  • 负载均衡,防止数据倾斜
  • 方便便集群容灾调度region

每一个region维护着startRow与endRowKey,如果加入的数据符合某个region维护的rowKey范围,则该数据交给这个region维

5.2 手动指定预分区

create 'person','info1','info2',SPLITS => ['1000','2000','3000']

也可以把分区规则创建于文件中
文件内容

aaa
bbb
ccc
ddd

执行

create 'student','info',SPLITS_FILE => '/root/hbase/split.txt'

第 6 节 Region 合并


6.1 Region合并说明

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

6.2 如何进⾏行行Region合并

通过Merge类冷合并Region
需要先关闭hbase集群
需求:需要把student表中的2个region数据进行合并:student,,1593244870695.10c2df60e567e73523a633f20866b4b5.
student,1000,1593244870695.0a4c3ff30a98f79ff6c1e4cc927b3d0d.
这里通过org.apache.hadoop.hbase.util.Merge类来实现,不需要进入hbase shell,直接执行(需要先关闭hbase集群):

hbase org.apache.hadoop.hbase.util.Merge student \
student,,1595256696737.fc3eff4765709e66a8524d3c3ab42d59. \
student,aaa,1595256696737.1d53d6c1ce0c1bed269b16b6514131d0.

通过online_merge热合并Region
不需要关闭hbase集群,在线进行合并

与冷合并不同的是,online_merge的传参是Region的hash值,而Region的hash值就是Region名称的最后那段在两个.之间的字符串部分。
需求:需要把lagou_s表中的2个region数据进行合并:
student,,1587392159085.9ca8689901008946793b8d5fa5898e06. \
student,aaa,1587392159085.601d5741608cedb677634f8f7257e000.
需要进入hbase shell:
merge_region 'c8bc666507d9e45523aebaffa88ffdd6','02a9dfdf6ff42ae9f0524a3d8f4c7777'

成功后观察界面

第三部分 HBase API应⽤用和优化

第 1 节 HBase API客户端操作

创建Maven工程,添加依赖

<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>
</dependencies>
package com.lagou.hbase.client;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.*;
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;

public class HbaseClientDemo {
    Configuration conf = null;
    Connection conn = null;

    @Before
    public void init() throws IOException {
        //获取一个配置文件对象
        conf = HBaseConfiguration.create();

        conf.set("hbase.zookeeper.quorum", "linux121,linux122");
        conf.set("hbase.zookeeper.property.clientPort", "2181");
        //通过conf获取到hbase集群的连接
        conn = ConnectionFactory.createConnection(conf);
    }

    //创建一张hbase表
    @Test
    public void createTable() throws IOException {
        //获取HbaseAdmin对象用来创建表
        HBaseAdmin admin = (HBaseAdmin) conn.getAdmin();
        //创建Htabledesc描述器,表描述器
        final HTableDescriptor worker = new HTableDescriptor(TableName.valueOf("worker"));
        //指定列族
        worker.addFamily(new HColumnDescriptor("info"));
        admin.createTable(worker);
        System.out.println("worker表创建成功!!");
    }

    //插入一条数据
    @Test
    public void putData() throws IOException {
        //需要获取一个table对象
        final Table worker = conn.getTable(TableName.valueOf("worker"));

        //准备put对象
        final Put put = new Put(Bytes.toBytes("110"));//指定rowkey

        put.addColumn(Bytes.toBytes("info"), Bytes.toBytes("addr"), Bytes.toBytes("beijing"));
        //插入数据,参数类型是put
        worker.put(put);
        //准备list<puts>,可以执行批量插入
        //关闭table对象
        worker.close();
        System.out.println("插入数据到worker表成功!!");
    }


    //删除一条数据
    @Test
    public void deleteData() throws IOException {
        //需要获取一个table对象
        final Table worker = conn.getTable(TableName.valueOf("worker"));

        //准备delete对象
        final Delete delete = new Delete(Bytes.toBytes("110"));
//执行删除
        worker.delete(delete);
        //关闭table对象
        worker.close();
        System.out.println("删除数据成功!!");
    }

    //查询数据
    @Test
    public void getData() throws IOException {
        //准备table对象
        final Table worker = conn.getTable(TableName.valueOf("worker"));
        //准备get对象
        final Get get = new Get(Bytes.toBytes("110"));
        //指定查询某个列族或者列
        get.addFamily(Bytes.toBytes("info"));
        //执行查询
        final Result result = worker.get(get);
        //获取到result中所有cell对象
        final Cell[] cells = result.rawCells();
        //遍历打印
        for (Cell cell : cells) {
            final String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
            final String f = Bytes.toString(CellUtil.cloneFamily(cell));
            final String column = Bytes.toString(CellUtil.cloneQualifier(cell));
            final String value = Bytes.toString(CellUtil.cloneValue(cell));

            System.out.println("rowkey-->" + rowkey + "--;cf-->" + f + "---;column--->" + column + "--;value-->" + value);
        }
        worker.close();
    }

    //全表扫描
    @Test
    public void scanData() throws IOException {
        //准备table对象
        final Table worker = conn.getTable(TableName.valueOf("worker"));
        //准备scan对象
        final Scan scan = new Scan();

        //执行扫描
        final ResultScanner resultScanner = worker.getScanner(scan);
        for (Result result : resultScanner) {
            //获取到result中所有cell对象
            final Cell[] cells = result.rawCells();
            //遍历打印
            for (Cell cell : cells) {
                final String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
                final String f = Bytes.toString(CellUtil.cloneFamily(cell));
                final String column = Bytes.toString(CellUtil.cloneQualifier(cell));
                final String value = Bytes.toString(CellUtil.cloneValue(cell));
                System.out.println("rowkey-->" + rowkey + "--;cf-->" + f + ";column--->" + column + "--;value-->" + value);
            }
        }

        worker.close();
    }
//指定scan 开始rowkey和结束rowkey,这种查询方式建议使用,指定开始和结束rowkey区间避免全表扫描
@Test
public void scanStartEndData() throws IOException {
    //准备table对象
    final Table worker = conn.getTable(TableName.valueOf("worker"));
    //准备scan对象
    final Scan scan = new Scan();
    //指定查询的rowkey区间,rowkey在hbase中是以字典序排序
    scan.setStartRow(Bytes.toBytes("001"));
    scan.setStopRow(Bytes.toBytes("004"));
    //执行扫描
    final ResultScanner resultScanner = worker.getScanner(scan);
    for (Result result : resultScanner) {
        //获取到result中所有cell对象
        final Cell[] cells = result.rawCells();
        //遍历打印
        for (Cell cell : cells) {
            final String rowkey = Bytes.toString(CellUtil.cloneRow(cell));
            final String f = Bytes.toString(CellUtil.cloneFamily(cell));
            final String column = Bytes.toString(CellUtil.cloneQualifier(cell));
            final String value = Bytes.toString(CellUtil.cloneValue(cell));
            System.out.println("rowkey-->" + rowkey + "--;cf-->" + f + ";column--->" + column + "--;value-->" + value);
        }
    }

    worker.close();
}



    //释放连接
    @After
    public void realse() {
        if (conn != null) {
            try {
                conn.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

第 2 节 Hbase 协处理器

2.1 协处理器概述

官方地址

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

Hbase Coprocessor类似以下概念

  • 触发器和存储过程:一个Observer Coprocessor有些类似于关系型数据库中的触发器器,通过它我们可以在一些事件(如Get或是Scan)发生前后执行特定的代码。
  • Endpoint Coprocessor则类似于关系型数据库中的存储过程,因为它允许我们在RegionServer上直接对它存储的数据进行运算,而非是在客户端完成运算。
  • MapReduce:MapReduce的原则就是将运算移动到数据所处的节点。Coprocessor也是按照相同的原则去工作的。
  • AOP:如果熟悉AOP的概念的话,可以将Coprocessor的执行过程视为在传递请求的过程中对请求进⾏行了拦截,并执行了了一些自定义代码。

2.2 协处理理器器类型

Observer

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

协处理器框架提供的接口如下

  • RegionObserver:用户可以用这种的处理理器器处理数据修改事件,它们与表的region联系紧密。
  • MasterObserver:可以被用作管理理或DDL类型的操作,这些是集群级事件。
  • WALObserver:提供控制WAL的钩子函数

Endpoint

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

Endpoint常见用途
聚合操作

  • 假设需要找出一张表中的最大数据,即 max 聚合操作,普通做法就是必须进行全表扫描,然后Client代码内遍历扫描结果,并执行求最大值的操作。这种方式存在的弊端是无法利用底层集群的并发运算能力,把所有计算都集中到 Client 端执行,效率低下。
  • 使用Endpoint Coprocessor,用户可以将求最大值的代码部署到 HBase RegionServer 端,HBase 会利用集群中多个节点的优势来并发执行求最大值的操作。也就是在每个 Region 范围内执行求最大值的代码,将每个 Region 的最大值在 Region Server 端计算出,仅仅将该 max 值返回给Client。在Client进一步将多个 Region 的最大值汇总进一步找到全局的最大值。

Endpoint Coprocessor的应用我们后续可以借助于Phoenix非常容易易就能实现。针对Hbase数据集进行聚合运算直接使用SQL语句句就能搞定。

2.3 Observer 案例

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

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

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

package com.lagou.hbase.processor;

import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.client.Durability;
import org.apache.hadoop.hbase.client.HTable;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.coprocessor.BaseRegionObserver;
import org.apache.hadoop.hbase.coprocessor.ObserverContext;
import org.apache.hadoop.hbase.coprocessor.RegionCoprocessorEnvironment;
import org.apache.hadoop.hbase.regionserver.wal.WALEdit;
import org.apache.hadoop.hbase.util.Bytes;

import java.io.IOException;
import java.util.List;


//重写prePut方法,监听到向t1表插入数据时,执行向t2表插入数据的代码
public class MyProcessor extends BaseRegionObserver {
    @Override
    public void prePut(ObserverContext<RegionCoprocessorEnvironment> e, Put put, WALEdit edit, Durability durability) throws IOException {
       //把自己需要执行的逻辑定义在此处,向t2表插入数据,数据具体是什么内容与Put一样
        //获取t2表table对象
        final HTable t2 = (HTable) e.getEnvironment().getTable(TableName.valueOf("t2"));
        //解析t1表的插入对象put
        final Cell cell = put.get(Bytes.toBytes("info"), Bytes.toBytes("name")).get(0);
        //table对象.put
        final Put put1 = new Put(put.getRow());
        put1.add(cell);
        t2.put(put1); //执行向t2表插入数据
        t2.close();
    }
}

添加依赖

<!-- https://mvnrepository.com/artifact/org.apache.hbase/hbase-server -->
<dependency>
    <groupId>org.apache.hbase</groupId>
    <artifactId>hbase-server</artifactId>
    <version>1.3.1</version>
</dependency>

2. 打成Jar包,上传HDFS

cd /opt/lagou/softwares
mv original-hbaseStudy-1.0-SNAPSHOT.jar processor.jar
hdfs dfs -mkdir -p /processor
hdfs dfs -put processor.jar /processor


3. 挂载协处理器

hbase(main):056:0> describe 't1'
hbase(main):055:0> alter 't1',METHOD =>
'table_att','Coprocessor'=>'hdfs://linux121:9000/processor/processor.jar|com.lagou.hbase.processor.MyProcessor|1001|'
#再次查看't1'表,
hbase(main):043:0> describe 't1'

4.验证协处理理器器
向t1表中插入数据(shell方式验证)

put 't1','rk1','info:name','lisi'

5.卸载协处理器

disable 't1'
alter 't1',METHOD=>'table_att_unset',NAME=>'coprocessor$1'
enable 't2'

第 4 节 HBase表的RowKey设计

RowKey的基本介绍
ASCII码字典顺序。
012,0,123,234,3.
0,3,012,123,234
0,012,123,234,3
字典序的排序规则。
先比较⼀一个字节,如果相同,然比对第二个字节,以此类推,
如果到第X个字节,其中一个已经超出了rowkey的长度,短rowkey排在前面。
2.1 RowKey长度原则
rowkey是一个二进制码流,可以是任意字符串,最大长度64kb,实际应⽤用中一般为10-100bytes,以byte[]形式保存,一般设计成定长。
建议越短越好,不要超过16个字节
设计过长会降低memstore内存的利利⽤用率和HFile存储数据的效率。
2.2 RowKey散列原则
建议将rowkey的⾼高位作为散列字段,这样将提高数据均衡分布在每个RegionServer,以实现负载均衡的几率。
2.3 RowKey唯一原则
必须在设计上保证其唯一性, 访问hbase table中的行:有3种方式:
单个rowkey
rowkey 的range
全表扫描(一定要避免全表扫描)
实现方式:
1)org.apache.hadoop.hbase.client.Get
2)scan方法: org.apache.hadoop.hbase.client.Scan
scan使用的时候注意:
setStartRow,setEndRow 限定范围, 范围越小,性能越高。
2.4 RowKey排序原则
HBase的Rowkey是按照ASCII有序设计的,我们在设计Rowkey时要充分利用这点.

第 5 节 HBase表的热点


5.1 什么是热点

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

5.2 热点的解决方案

预分区

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

加盐

  • 这里所说的加盐不是密码学中的加盐,而是在rowkey的前面增加随机数,具体就是给rowkey分配一个随机前缀以使得它和之前的rowkey的开头不不同。

4个region,[,a),[a,b),[b,c),[c,]
原始数据:abc1,abc2,abc3.
加盐后的rowkey:a-abc1,b-abc2,c-abc3
abc1,a
abc2,b
哈希

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

原始数据: abc1,abc2,abc3
哈希:
md5(abc1)=92231b....., 9223-abc1
md5(abc2) =32a131122...., 32a1-abc2
md5(abc3) = 452b1...., 452b-abc3.
反转

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


第 6 节 HBase的二级索引


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

第 7 节 布隆过滤器在hbase的应用

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


Bloom Filter案例例
布隆过滤器,已经不需要自己实现,Google已经提供了非常成熟的实现。
 

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>27.0.1-jre</version>
</dependency>

使用guava 的布隆过滤器,封装的非常好,使用起来非常简洁方便。
例: 预估数据量量1w,错误率需要减小到万分之一。使⽤用如下代码进⾏创建。

package com.lagou.hbase.bloom;

import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

import java.nio.charset.Charset;

public class BloomFilterDemo {

    public static void main(String[] args) {
        // 1.创建符合条件的布隆过滤器
        // 预期数据量10000,错误率0.0001
        BloomFilter<CharSequence> bloomFilter =
                BloomFilter.create(Funnels.stringFunnel(Charset.forName("utf-8")), 10000, 0.0001);
        // 2.将一部分数据添加进去
        for (int i = 0; i < 5000; i++) {
            bloomFilter.put("" + i);
        }
        System.out.println("数据写入完毕");
        // 3.测试结果
        for (int i = 0; i < 10000; i++) {
            if (bloomFilter.mightContain("" + i)) {
                System.out.println(i + "存在");
            } else {
                System.out.println(i + "不存在");
            }
        }
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值