浅谈hbase

hbase
非关系型可实时读写分布式列式存储数据库(列可以动态添加),底层存储可以使用本地文件系统也可以使用hdfs
列式数据库的优点:随着业务的增加可以改变数据库的列,而且列中没有数据就没有对应的单元格,列式数据库比较灵活而且节省空间,在定义表的时候不需要指定列,只在插入数据的时候动态来添加列,如果数据比较大了可以横向的来添加存储,
只要能将数据转换成二进制,然后通过二进制流的方式就可以写到hbase中
模型
    rowKey:唯一标识一行数据,按照字典排序,每个rowkey只能存储64K的字节数据(根据业务场景越短越好)
    timeStamp:版本,每插入一个单元格的数据就会有个版本,可以用默认的(就是时间戳,hbase插入数据时自动给出)也可以自定义自己来标识,一般使用默认的。最新的版本一定在最前面,数据最大的版本数只有一个
    columnFamily:列族(一般设置2-3个即可,太多支持的不是很好),在定义表的时候列族一定要作为表的schema给出,每个列族可以有多个列成员,可以后续添加列。权限的控制、存储、调优都是在列族层面进行的。hbase在储存的时候会将同一列族下的数据放在同一个目录下面(hbase中有几个列族,那么底层就有几个目录,目录下面就是存放的数据文件)
    cell:单元格,一个单元格需要rowKey,版本,列族,列名四个来确定一个单元格,单元格中存放的数据就是未解析的字节数组,没有类型可言
    hlog:日志,记录hbase的所有操作,尤其是写操作(不止记录操作还会将数据一起记录到日志文件中),主要目的是用来备份,正常情况下数据都在内存中,但是断电后内存的数据就没了,可以通过hlog进行恢复
架构
    zookeeper:保证集群中只有一个处于active状态的hMaster,存储所有hRegion的寻址入口,实时监控regionServer的监控状况,并实时通知hMaster,存储hbase的schema和表元数据信息
    hMaster:负责接收客户端的请求(主节点),hMaster和hRegionServer是一对多的关系,可以为hRegionServer分配hRegion,负责RegionServer的负载均衡(RegionServer中的region个数不均衡),如果RegionServer中region比较多会将region迁移到负载比较低的RegionServer中,发现失效的RegionServer会将其region重新分配给其他RegionServer
    hRegionServer:负责处理io请求(从节点),可以有多个hRegion,负责切分在运行过程中变的过大的region,尽量保持等分的原则
    hRegion:每个hRegion里面有一个hlog和多个store,hbase中会将表水平的划分成多个区域,一个region存储一个区域中的数据,当region中个数很多的时候master也会将region进行迁移到负载比较低的regionServer中。region是hbase中分布式存储和负载均衡的最小单元
    store:每个store对应一个列族,每个store中有一个MemStore(内存缓存区)和多个storeFile(如果底层存储在Hadoop中,那么站在Hadoop角度这个storeFile叫做hFile)
    MemStore:内存缓冲区,数据会先写到MemStore中,当MemStore中数据达到一定阈值时regionServer会启动flashcache进程溢写进行持久化生成storeFile文件。客户端检索数据的时候会先在memStore中检索,没有才会去storeFile中
    storeFile:真实存放数据,在hdfs中或者本地文件系统中,当溢写的storeFile数量增长到一定阈值后,系统会自动合并,在合并过程中会进行版本合并和删除工作,形成更大的storeFile
搭建(官网->Documentation and API->Basic Prerequisites查看hbase和jdk、Hadoop的版本兼容情况,笔者采用2.1.2)
1.伪分布式(官网->Documentation and API->Quick Start - Standalone HBase,依赖jdk)
    hbase自己已经内置了zookeeper,所以不需要额外搭建zookeeper(建议测试情况下使用)
    1)修改conf/hive-env.sh
        export JAVA_HOME=/usr/java/default
    2)修改conf/hive-site.xml
    <configuration>
      <property>
        <!--底层数据存放位置可以是本地文件系统,也可以是hdfs,保证目标不存在或者为空-->
        <name>hbase.rootdir</name>
        <value>file:///opt/hbase</value>
      </property>
      <property>
        <!--zookeeper内存缓冲区数据溢写目录,空或者不存在-->
        <name>hbase.zookeeper.property.dataDir</name>
        <value>/opt/zookeeper</value>
      </property>
      <property>
        <!--如果使用本地文件系统存储,必须配置该项并设置为false-->
        <name>hbase.unsafe.stream.capability.enforce</name>
        <value>false</value>
      </property>
    </configuration>
    3)启动hbase
        bin/start-hbase.sh
      关闭hbase
        bin/stop-hbase.sh
    4)浏览器访问
        HMaster_IP:16010

2.分布式(官网->Documentation and API->HBase run modes: Standalone and Distributed,依赖jdk,Hadoop,zookeeper)
    1)master节点做免密钥,并将master节点的公钥文件追加到RegionServer节点的authorized_keys中
        ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
        cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
    2)修改vi conf/hbase-env.sh
        export JAVA_HOME=/usr/java/default
        export HBASE_MANAGES_ZK=false设置为false不启用内置的zookeeper
        (export HADOOP_CONF_DIR=/opt/hadoop-2.7.5/etc/hadoop如果使用Hadoop高可靠用需要配置或者将hdfs-site拷贝到hbase配置目录)
        (export HBASE_CLASSPATH=$HADOOP_CONF_DIR并将HADOOP_CONF_DIR加载到hbase可变变量中)
    3)修改vi conf/hbase-site.xml
        <configuration>
          <property>
            <!--配置zookeeper溢写路径,如果写了这个配置需要和zk集群配置路径一样,否则就不要写,自己会去找-->
            <name>hbase.zookeeper.property.dataDir</name>
            <value>/home/testuser/zookeeper</value>
          </property>
          <property>
            <!--设置hbase底层数据存储路径,目录为空或者不存在,如果时高可用的HDFS,可以直接写nameservices,但需要将hdfs-site拷贝到hbase配置目录中,-->
            <!--或者hbase-env.sh文件中添加Hadoop配置路径export HADOOP_CONF_DIR=/opt/hadoop-2.7.5/etc/hadoop和HBASE_CLASSPATH-->
            <name>hbase.rootdir</name>
            <value>hdfs://mycluster/hbase</value>
          </property>
          <property>
              <!--设置是否为分布式-->
            <name>hbase.cluster.distributed</name>
            <value>true</value>
          </property>
          <property>
              <!--zookeeper集群-->
            <name>hbase.zookeeper.quorum</name>
            <value>zk1,zk2,zk3</value>
          </property>
        </configuration>
    4)在conf/regionservers文件中添加regionserver节点
        node1
        node2
        node3
    5)手动创建conf/backup-masters文件,用来存储备份MASTER_IP
        node2
    6)分发节点
    7)启动hbase(master节点上启动)
        bin/start-hbase.sh
      关闭hbase
        bin/stop-hbase.sh
    8)浏览器访问
        HMaster_IP:16010
        备份HMaster_IP:16010
HbaseShell(执行的时候没有分号,如果写了删除ctrl+backspace)
    进入hbase shell
        bin/hbase shell
    退出hbase shell
        hbase(main):001:0> quit
    常用命令
    DDL
        查看帮助
            help
        创建表(必须声明列族)
            create '表名','列族1','列族2'
        删除表(必须先禁用表才可删除)
            drop '表名'
        删除所有表
            drop_all
        禁用表(禁用后不能对表进行读写)
            disable '表名'
        禁用所有表
            disable_all
        开放表
            enable '表名'
        开放所有表    
            enable_all
        查看表描述(里面的TTL是存储数据的生命周期,默认forever永久保存,可设置;blocksize缓存大小;
                    in_memory缓存是否加载到内存,默认false在磁盘中,生产环境一般设置打开;
                    blockcache是否启用缓存,默认true)
            describe '表名'
        查看所在namespace(相当于库)中有哪些表
            list
        查看表是否存在
            exits '表名'
    NameSpace(相当于库,创建表不能放在hbase库中)
        查看有哪些namespace
            list_namespace
        创建namespace
            create_namespace 'namespace_name'
    DML
        插入数据(修改数据也用put,底层会添加一条版本更高的数据)
            put '表名','rowkey','列族:列','值'
        全表扫描,输出表中所有数据(不会经常使用,消耗性能比较大)
            scan '表名'
        获取数据(一个单元格数据)
            get '表名','rowkey','列族:列'
        删除数据(一个单元格数据)
            delete '表名','rowkey','列族:列'
        删除表中所有数据
            deleteall '表名','rowkey'    删除一行数据
            deleteall '表名','rowkey','列族'    删除指定列族中所有列
        统计表中行数
            count '表名'
        截断表
            truncate '表名'
    Tools
        强制将表数据溢写到磁盘
            flush '表名'  
hbase存储数据的时候,同一个单元格中插入多次数据,只是将最新的数据设置最高的版本,并没有覆盖单元格,触发合并的时候会自己将低版本数据删除
真实目录和hbase模型的对应关系
    data目录的子目录对应namespace
    namespace目录的子目录对应表
    表目录的子目录对应regionserver
    regionserver目录的子目录对应列族
    列族目录的文件就是每个单元格对应的数据就是磁盘文件,如果没有那么还没有溢写,可强制溢写磁盘查看
    可以通过"bin/hbase hfile -pf 文件路径"命令来查看文件内容,每个单元格为一个文件
Java操作Hbase
    代码示例:
    public class HbaseTest {
        private Admin admin;
        private HBaseConfiguration conf;
        private Connection connection;
        private TableName tableName;
        private Table table;
        @Before
        public void setup() throws Exception {
            conf = new HBaseConfiguration();
            //如果不设置zookeeper,那么需要将hbase-site.xml和hdfs-site.xml拷贝到src目录
            conf.set("hbase.zookeeper.quorum","node1,node2,node3");
            connection = ConnectionFactory.createConnection(conf);
            admin = connection.getAdmin();
            tableName = TableName.valueOf("phone");
        }
        @After
        public void end() throws Exception {
            if(admin != null) {
                admin.close();
            }
            if(table != null) {
                table.close();
            }
        }
        @Test
        public void createTbl() throws Exception {
            if(admin.tableExists(tableName)) {
                admin.disableTable(tableName);
                admin.deleteTable(tableName);
            }
            //表描述器构造器
            TableDescriptorBuilder tdb  =TableDescriptorBuilder.newBuilder(tableName);
            //列族描述起构造器
            ColumnFamilyDescriptorBuilder cfdb =  ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("cf1"));
            //对列族设置属性
            cfdb.setBlockCacheEnabled(true);//是否启动缓存
            cfdb.setInMemory(true);//是否使用内存做为缓存
            cfdb.setMaxVersions(1);//设置最大的版本号,设置1表示只有一个版本
            //获得列描述起
            ColumnFamilyDescriptor  cfd = cfdb.build();
            //添加列族
            tdb.setColumnFamily(cfd);
            //获得表描述器
            TableDescriptor td = tdb.build();
            admin.createTable(td);
            System.out.println("success !!!");
        }
        @Test
        public void insertDB() throws Exception {
            //rowkey  手机号+时间
            String rowkey = "18612341234_201609181652";
            Put put = new Put(rowkey.getBytes());
            //添加单元格,参数分别为:列族、列、值
            put.addColumn("cf1".getBytes(), "phone_number".getBytes(), "17012341234".getBytes());
            put.addColumn("cf1".getBytes(), "time".getBytes(), "201609181652".getBytes());
            put.addColumn("cf1".getBytes(), "type".getBytes(), "1".getBytes());
            table = connection.getTable(tableName);
            table.put(put);
            System.out.println("success !!!");
        }
        @Test
        public void getDB() throws Exception {
            table = connection.getTable(tableName);
            // rowkey  手机号+时间
            String rowkey = "18612341234_201609181652";
            Get get = new Get(rowkey.getBytes());
            //填加需要查询的单元格的列族、列
            get.addColumn("cf1".getBytes(),"phone_number".getBytes());
            //获取查询到的所有版本的值
            Result rs = table.get(get);
            if(rs.isCursor()){
                System.out.println("null");
            }
            else{
                //从查询结果中获取版本最新的数据,或者CellUtil来取值
                Cell cell = rs.getColumnLatestCell("cf1".getBytes(), "phone_number".getBytes());
                //根据偏移量寻找列和值
                String col = Bytes.toString(cell.getQualifierArray(), cell.getQualifierOffset(), cell.getQualifierLength());
                String value = Bytes.toString(cell.getValueArray(), cell.getValueOffset(), cell.getValueLength());
                System.out.println(col+"::"+value);
                System.out.println("success !!!");
            }
        }
        @Test//插入 十个手机号 每个手机号有一百条通话记录
        public void insertDB2() throws Exception {
            List<Put> puts = new ArrayList<Put>();
            for (int i = 0; i < 10; i++) {
                String phonenum = getPhone("186");
                for (int j = 0; j < 100; j++) {
                    String datestr = getDate("2016");
                    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
                    long dateLong = sdf.parse(datestr).getTime();
                    // 时间降序排序  Long.MAX_VALUE-时间戳
                    String rowkey = phonenum + "_" + (Long.MAX_VALUE-dateLong);
                    Put put = new Put(rowkey.getBytes());
                    put.addColumn("cf1".getBytes(), "phone_number".getBytes(), phonenum.getBytes());
                    put.addColumn("cf1".getBytes(), "time".getBytes(), datestr.getBytes());
                    put.addColumn("cf1".getBytes(), "type".getBytes(), (r.nextInt(2)+"").getBytes());
                    puts.add(put);
                }
            }
            table = connection.getTable(tableName);
            table.put(puts);
            System.out.println("success !!!");
        }
        //获取某号码某段时间内所有通话详单(hbase中rowkey默认已经排好序了)
        @Test//利用scan根据rowkey的范围进行取值
        public void scanDB() throws Exception {
            table = connection.getTable(tableName);
            Scan scan = new Scan();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
            //设置rowkey开始位置
            String startRow = "18695370905_" + (Long.MAX_VALUE-sdf.parse("20160630235959").getTime());
            scan.setStartRow(startRow.getBytes());
            //设置rowkey结束位置
            String stopRow = "18695370905_" + (Long.MAX_VALUE-sdf.parse("20160101000000").getTime());;
            scan.setStopRow(stopRow.getBytes());
            ResultScanner rss = table.getScanner(scan);
            for (Result rs : rss) {
                String phone_number = new String(CellUtil.cloneValue(rs.getColumnLatestCell("cf1".getBytes(), "phone_number".getBytes())));
                String time = new String(CellUtil.cloneValue(rs.getColumnLatestCell("cf1".getBytes(), "time".getBytes())));
                String type = new String(CellUtil.cloneValue(rs.getColumnLatestCell("cf1".getBytes(), "type".getBytes())));
                System.out.println(phone_number+'_'+time+'_'+type);
            }
            System.out.println("success !!!");
        }
        //获取某号码type为1的通话详单
        @Test//通过过滤器对值进行过滤
        public void scanDB2() throws Exception {
            table = connection.getTable(tableName);
            Scan scan = new Scan();
            //初始化过滤器,MUST_PASS_ALL表示所有条件都通过才会返回,MUST_PASS_ONE表示通过其中一个条件就会返回
            FilterList list = new FilterList(FilterList.Operator.MUST_PASS_ALL);
            //过滤条件,PrefixFilter是对rowkey的前缀进行过滤,符合则通过
            PrefixFilter prefixFilter = new PrefixFilter("18695370905".getBytes());
            //过滤条件,SingleColumnValueFilter过滤某列的值大小范围是多少
            SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter(
                    "cf1".getBytes(),//指定列族
                    "type".getBytes(),//指定列
                    CompareOp.EQUAL,//指定比较运算符 条件,EQUAL等于;GREATER大于;GREATER_OR_EQUAL大于等于;LESS小于;LESS_OR_EQUAL小于等于;NOT_EQUAL不等于
                    Bytes.toBytes("1")//指定值
                    );
            //过滤器添加过滤条件
            list.addFilter(prefixFilter);
            list.addFilter(singleColumnValueFilter);
            //添加过滤器
            scan.setFilter(list);
            ResultScanner rss = table.getScanner(scan);
            for (Result rs : rss) {
                String phone_number = new String(CellUtil.cloneValue(rs.getColumnLatestCell("cf1".getBytes(), "phone_number".getBytes())));
                String time = new String(CellUtil.cloneValue(rs.getColumnLatestCell("cf1".getBytes(), "time".getBytes())));
                String type = new String(CellUtil.cloneValue(rs.getColumnLatestCell("cf1".getBytes(), "type".getBytes())));
                System.out.println(phone_number+'_'+time+'_'+type);
            }
            System.out.println("success !!!");
        }
        
        Random r = new Random();
        //返回随机手机号
        public String getPhone(String pre) {
            return pre + String.format("%08d", r.nextInt(99999999));
        }
        //返回随机年月日时分秒
        public String getDate(String year) {
            return year + String.format("%02d%02d%02d%02d%02d", new Object[]{r.nextInt(12)+1, r.nextInt(30)+1,
                    r.nextInt(60), r.nextInt(60), r.nextInt(60)});
        }    
    }
hbase可查看官网->Documentation and API->Client Request Filters过去器
对上面的代码做进一步的优化,使用ProtocolBuffler将一天内所有的话单数据封装在一个ProtocolBuffler对象中,将一天的通话详单再封装成一个对象,节省存储空间
    ProtocolBuffler一种轻便高效的结构化数据存储格式
    http://code.google.com/p/protobuf/downloads/list可下载Protobuf的源代码
    安装(在随意一个节点上进行)
    需要先安装编译环境,笔者这里为了方便直接安装了开发者工具包
    yum install Development tools -y
    解压,编译安装
    tar -xzf protobuf-2.1.0.tar.gz
    cd protobuf-2.1.0
    ./configure --prefix=可以指定安装位置
    make
    make check
    make install
    任意目录下编写以.proto结尾的proto文件(笔者这里安装上面的例子来使用),内容如下
        package hbaseTest;(指定包,保持和上面代码一样,笔者这里是hbaseTest)
        message phone(相当于类名,随意写)
        {(下面相当于属性,后面的1,2,3是序列化时候的顺序;required是必须的,optional是可选的,注意protobuf里面整型类型是int32)
            required string phone_number=1;
            required string time=2;
            required string type=3;
        }
        (上面是对每一条通话详单进行封装,下面是对一天的通话详单进行封装)
        message phone_day
        {(这里因为存储一天的通话详单就相当于是一个list,protobuf里面是repeated类型)
             repeated phone phones=1;
        }
    将proto文件转成Java文件
    bin/protoc -I=proto文件所在目录 --java_out=输出位置 proto文件位置的绝对路径        
    在输出目录下会出现和定义package名字相同的目录,里面就是生成的Java文件
    将java文件拷贝到Java项目中,并在原基础上添加测试代码如下
    @Test//插入一天 十个手机号 每个一百条通话记录,并将一个手机号一天内的通话详单放到一个对象中
    public void insertDB3() throws Exception {
        table = connection.getTable(tableName);
        for (int i = 0; i < 10; i++) {
            String phonenum = getPhone("157");
            String datestr = getDate2("20160919");//获取一天
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
            //初始化ProtocolBuffler生成的对象
            Phone.phone_day.Builder day_phone = Phone.phone_day.newBuilder();
            // 时间降序排序  Long.MAX_VALUE-时间戳
            String rowkey = phonenum + "_" + (Long.MAX_VALUE-sdf.parse(datestr).getTime());
            //将9月19号一天的通话详单封装到day_phone对象中
            for (int j = 0; j < 100; j++) {
                //会自动生成javaBean对象
                Phone.phone.Builder phone = Phone.phone.newBuilder();
                phone.setPhoneNumber(phonenum);
                phone.setTime(datestr);
                phone.setType((r.nextInt(2)+""));
                day_phone.addPhones(phone);
            }
            Put put = new Put(rowkey.getBytes());
            //第三个参数转成转成字节数组
            put.addColumn("cf1".getBytes(), "day_phone".getBytes(), day_phone.build().toByteArray());
            table.put(put);
        }
        System.out.println("success !!!");
    }
    @Test//获取数据
    public void getDB2() throws Exception {
        table = connection.getTable(tableName);
        // rowkey  手机号+时间
        String rowkey = "15776231939_9223370562518168807";
        Get get = new Get(rowkey.getBytes());
        get.addColumn("cf1".getBytes(), "day_phone".getBytes());
        Result rs = table.get(get);
        if(rs.isEmpty()){
            System.out.println("null");
        }else{
            Cell cell = rs.getColumnLatestCell("cf1".getBytes(), "day_phone".getBytes());
            Phone.phone_day day_phone = Phone.phone_day.parseFrom(CellUtil.cloneValue(cell));//对字节数组转成day_phone对象
            for (Phone.phone phone : day_phone.getPhonesList()) {//遍历day_phone对象转成后的list对象
                System.out.println(phone.getPhoneNumber() + "-" + phone.getTime() + "-" + phone.getType());
            }
            System.out.println("success !!!");
        }

    }
    public String getDate2(String str) {//获取一天,时分秒
        return str + String.format("%02d%02d%02d", new Object[]{r.nextInt(60), r.nextInt(60), r.nextInt(60)});
    }
简单建表示例
1)人员-角色表,条件:
    查询一个人有哪些角色
    查询一个角色下有哪些人
    角色有优先级
    人员添加删除角色
    人员 删除 添加
    角色 删除 添加
建立两张表
    角色表roletable
    rowkey:rid,cf1:pid=pname  cf2:rname=角色名称
    人员表persontable
    rowkey:pid,cf1:rid=优先级 cf2:pname=人员名称
2)部门表
    查询顶级部门
    查询子部门
    查询父部门
    删除、增加子部门
建立表
    部门表dtable
    rowkey:(0|1_部门编号)=部门did
    列族1:cf1:dname=部门名称,parentid=父部门did...
    列族2:cf2:did=dname|0
3)微博
    查看微博首页,所关注的用户发布的最新微博
    关注添加|删除关注,查看关注列表
    粉丝,查看粉丝列表
    发布微博
建立表
    粉丝关注表
    rowkey:rid(用户id) cf1(粉丝):rid=rname... cf2(关注):rid=rname...
    微博表
    rowk :rid_(long.max-now)=wid cf1:content=微博内容
    收微博表
    rowkey:rid cf1:wb=wid version=1000
Hbase优化
1.表设计
    1)Pre-Creating Regions创建预分区
    官网->Documentation and API->Reference Guide->Writing to HBase->Table Creation: Pre-Creating Regions
    默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样当数据写入HBase时,会按照region分区情况,在集群内做数据的负载均衡。
    //创建表,参数中添加字节二维数组
    public static boolean createTable(HBaseAdmin admin, HTableDescriptor table, byte[][] splits)throws IOException {
      try {
        admin.createTable(table, splits);//splits是一个二维数组
        return true;
      } catch (TableExistsException e) {
        logger.info("table " + table.getNameAsString() + " already exists");
        // the table already exists...
        return false;  
      }
    }
    获取上面二维数组的方法(主要针对rowkey都是数字),就是做切分
    public static byte[][] getHexSplits(String startKey, String endKey, int numRegions) { //start:001,endkey:100,10region [001,010][011,020]
      byte[][] splits = new byte[numRegions-1][];
      BigInteger lowestKey = new BigInteger(startKey, 16);
      BigInteger highestKey = new BigInteger(endKey, 16);
      BigInteger range = highestKey.subtract(lowestKey);
      BigInteger regionIncrement = range.divide(BigInteger.valueOf(numRegions));
      lowestKey = lowestKey.add(regionIncrement);
      for(int i=0; i < numRegions-1;i++) {
        BigInteger key = lowestKey.add(regionIncrement.multiply(BigInteger.valueOf(i)));
        byte[] b = String.format("%016x", key).getBytes();
        splits[i] = b;
      }
      return splits;
    }
    也可直接手动split '表名'
    2)Row Key
    HBase中rowkey用来检索表中的记录,支持以下三种方式:
        通过单个rowkey访问:即按照某个rowkey键值进行get操作;
        通过rowkey的range进行scan:即通过设置startRowKey和endRowKey,在这个范围内进行扫描;
        全表扫描:即直接扫描整张表中所有行记录。
    在HBase中,rowkey可以是任意字符串,最大长度64KB,实际应用中一般为10~100bytes,存为byte[]字节数组,一般设计成定长的。
    rowkey是按照字典序存储,因此,设计rowkey时,要充分利用这个排序特点,将经常一起读取的数据存储到一块,将最近可能会被访问的数据放在一块。
    举个例子:如果最近写入HBase表中的数据是最可能被访问的,可以考虑将时间戳作为rowkey的一部分,由于是字典序排序,所以可以使用Long.MAX_VALUE - timestamp作为rowkey,这样能保证新写入的数据在读取时可以被快速命中。
    如果某字段的使用频率比较高也可以放到rowkey中,方便查询。
    设计Rowkey规则:
        满足业务需求的情况下,越小越好。
        散列性:将数据打散分散到多个region上,常用方式:
            对rowkey取反,取反后的rowkey会自动排序,顺序就会发生变化,rowkey会分散到不同的region中。
            对rowkey取hash值
        散列性好处:降低region的负载,坏处:查询的效率会减低
    3)Column Family列族
        一张表里不能定义太多的列族。目前Hbase并不能很好的处理超过2~3个列族的表。因为某个列族在flush的时候(列族对应的store中的memoryStore发生溢写),它邻近的列族也会因关联效应被触发flush,最终导致系统产生更多的I/O。
    4)InMermory读缓存,创建表的时候,可以通过HColumnDescriptor.setInMemory(true)将表放到RegionServer的缓存中,保证在读取的时候被cache命中。
    5)MaxVersion创建表的时候,可以通过HColumnDescriptor.setMaxVersions(int maxVersions)设置表中数据的最大版本,如果只需要保存最新版本的数据,那么可以设置setMaxVersions(1)
    6)TimeToLive创建表的时候,可以通过HColumnDescriptor.setTimeToLive(int timeToLive)设置表中数据的存储生命期,过期数据将自动被删除,例如如果只需要存储最近两天的数据,那么可以设置setTimeToLive(2 * 24 * 60 * 60)。
    7)Compact & Split(合并和分片)
        在HBase中,数据在更新时首先写入WAL 日志(HLog)和内存(MemStore)中,MemStore中的数据是排序的,当MemStore累计到一定阈值时,就会创建一个新的MemStore,并且将老的MemStore添加到flush队列,由单独的线程flush到磁盘上,成为一个StoreFile。于此同时, 系统会在zookeeper中记录一个RedoPoint,表示这个时刻之前的变更已经持久化了(minor compact)。
        StoreFile是只读的,一旦创建后就不可以再修改。因此Hbase的更新其实是不断追加的操作。当一个Store中的StoreFile达到一定的阈值后,就会进行一次合并(major compact),将对同一个key的修改合并到一起,形成一个大的StoreFile,当StoreFile的大小达到一定阈值后,又会对 StoreFile进行分割(split),等分为两个StoreFile。
        由于对表的更新是不断追加的,处理读请求时,需要访问Store中全部的StoreFile和MemStore,将它们按照row key进行合并,由于StoreFile和MemStore都是经过排序的,并且StoreFile带有内存中索引,通常合并过程还是比较快的。
    实际应用中,可以考虑必要时手动进行major compact,将同一个rowKey的修改进行合并形成一个大的StoreFile。同时,可以将StoreFile设置大些,减少split的发生。
    hbase为了防止小文件(被刷到磁盘的menstore)过多,以保证保证查询效率,hbase需要在必要的时候将这些小的storeFile合并成相对较大的storeFile,这个过程就称之为compaction。在hbase中,主要存在两种类型的compaction:minor  compaction和major compaction。
        minor compaction:minor合并一般是较小或很小文件的合并,一般用默认的即可,由以下几个参数共同决定:
            hbase.hstore.compaction.min :默认值为 3,表示至少需要三个满足条件的store file时,minor compaction才会启动
            hbase.hstore.compaction.max 默认值为10,表示一次minor compaction中最多选取10个store file
            上面两个表示一次合并的个数是3到10之间
            hbase.hstore.compaction.min.size 表示文件大小小于该值的store file 一定会加入到minor compaction的store file中
            hbase.hstore.compaction.max.size 表示文件大小大于该值的store file 一定会被minor compaction排除
            hbase.hstore.compaction.ratio 将store file 按照文件年龄排序(older to younger),minor compaction总是从older store file开始选择
        major compaction:major合并的功能是将所有的store file合并成一个,触发major compaction的可能条件有:major_compact 命令、majorCompact() API、region server自动运行(相关参数:hbase.hregion.majoucompaction 默认为24 小时、hbase.hregion.majorcompaction.jetter 默认值为0.2 防止region server 在同一时间进行major compaction)。
            hbase.hregion.majorcompaction.jetter参数的作用是:对参数hbase.hregion.majoucompaction规定的值起到浮动的作用,假如两个参数都为默认值24和0,2,那么major compact最终使用的数值为:19.2~28.8 这个范围。
            生产环境一般会关闭自动major compaction并手动编程major compaction,因为如果触发了major合并会将所有的store file合并在一起,当数据量很大时合并会非常耗时,而且这段事件不可以做其他操作。因此一般会在访问量小的时候通过Timer类或contab调用major_compact命令或majorCompact() API来触发
            如果必须让hbase中的regionServer自动运行合并可以配置:
                hbase.hregion.majoucompaction 默认为24 小时,设置多长周期触发一次
                hbase.hregion.majorcompaction.jetter 默认值为0.2 防止region server在同一时间进行major compaction
2.写表操作
    1)多table对象并发写,提高写数据的吞吐量,例如
        static final Configuration conf = HBaseConfiguration.create();
        static final String table_log_name = “user_log”;
        wTableLog = new HTable[tableN];
        for (int i = 0; i < tableN; i++) {
            wTableLog[i] = new HTable(conf, table_log_name);
            wTableLog[i].setWriteBufferSize(5 * 1024 * 1024); //5MB
            wTableLog[i].setAutoFlush(false);
        }
    2)table参数设置
        通过调用HTable.setAutoFlush(false)方法可以将HTable写客户端的自动flush关闭,这样可以批量写入数据到HBase,而不是有一条put就执行一次更新,只有当put填满客户端写缓存时,才实际向HBase服务端发起写请求。默认情况下auto flush是开启的。
        通过调用HTable.setWriteBufferSize(writeBufferSize)方法可以设置HTable客户端的写buffer大小,如果新设置的buffer小于当前写buffer中的数据时,buffer将会被flush到服务端。其中,writeBufferSize的单位是byte字节数,可以根据实际写入数据量的多少来设置该值。
        在HBae中,客户端向集群中的RegionServer提交数据时(Put/Delete操作),首先会先写WAL(Write Ahead Log)日志(即HLog,一个RegionServer上的所有Region共享一个HLog),只有当WAL日志写成功后,再接着写MemStore,然后客户端被通知提交数据成功;如果写WAL日志失败,客户端则被通知提交失败。这样做的好处是可以做到RegionServer宕机后的数据恢复。因此,对于相对不太重要的数据,可以在Put/Delete操作时,通过调用Put.setWriteToWAL(false)或Delete.setWriteToWAL(false)函数,放弃写WAL日志,从而提高数据写入的性能。
        谨慎选择关闭WAL日志,因为这样的话,一旦RegionServer宕机,Put/Delete的数据将会无法根据WAL日志进行恢复。
    3)批量写
        通过调用HTable.put(Put)方法可以将一个指定的row key记录写入HBase,同样HBase提供了另一个方法:通过调用HTable.put(List<Put>)方法可以将指定的row key列表,批量写入多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高,网络传输RTT高的情景下可能带来明显的性能提升。
    4)多线程并发写(也可以通过MapReduce来写)
        在客户端开启多个HTable写线程,每个写线程负责一个HTable对象的flush操作,这样结合定时flush和写buffer(writeBufferSize),可以既保证在数据量小的时候,数据可以在较短时间内被flush(如1秒内),同时又保证在数据量大的时候,写buffer一满就及时进行flush。
        例如:
        for (int i = 0; i < threadN; i++) {
            Thread th = new Thread() {
                public void run() {
                    while (true) {
                        try {
                            sleep(1000); //1 second
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
        synchronized (wTableLog[i]) {
                            try {
                                wTableLog[i].flushCommits();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
        }
            };
            th.setDaemon(true);
            th.start();
        }
3.读表操作
    1)多table并发读,提高读数据的吞吐量,例如
        static final Configuration conf = HBaseConfiguration.create();
        static final String table_log_name = “user_log”;
        rTableLog = new HTable[tableN];
        for (int i = 0; i < tableN; i++) {
            rTableLog[i] = new HTable(conf, table_log_name);
            rTableLog[i].setScannerCaching(50);
        }
    2)table参数设置
        hbase.client.scanner.caching配置项可以设置HBase scanner一次从服务端抓取的数据条数,默认情况下一次一条。通过将其设置成一个合理的值,可以减少scan过程中next()的时间开销,代价是scanner需要通过客户端的内存来维持这些被cache的行记录。有三个地方可以进行配置:1)在HBase的conf配置文件中进行配置;2)通过调用HTable.setScannerCaching(int scannerCaching)进行配置;3)通过调用Scan.setCaching(int caching)进行配置。三者的优先级越来越高。
        scan时指定需要的Column Family,可以减少网络传输数据量,否则默认scan操作会返回整行所有Column Family的数据。scan的优先级最高,table次之,配置文件最低
        通过scan取完数据后,记得要关闭ResultScanner,否则RegionServer可能会出现问题(对应的Server资源无法释放)。
    3)批量读
        通过调用HTable.get(Get)方法可以根据一个指定的row key获取一行记录,同样HBase提供了另一个方法:通过调用HTable.get(List<Get>)方法可以根据一个指定的row key列表,批量获取多行记录,这样做的好处是批量执行,只需要一次网络I/O开销,这对于对数据实时性要求高而且网络传输RTT高的情景下可能带来明显的性能提升。
    4)多线程并发读(也可以通过MapReduce来读)
        在客户端开启多个HTable读线程,每个读线程负责通过HTable对象进行get操作。下面是一个多线程并发读取HBase,获取店铺一天内各分钟PV值的例子:
        
    5)缓存查询结果
        对于频繁查询HBase的应用场景,可以考虑在应用程序中做缓存,当有新的查询请求时,首先在缓存中查找,如果存在则直接返回,不再查询HBase;否则对HBase发起读请求查询,然后在应用程序中将查询结果缓存起来。至于缓存的替换策略,可以考虑LRU等常用的策略。
    6)Blockcache
        HBase上Regionserver的内存分为两个部分,一部分作为Memstore,主要用来写;另外一部分作为BlockCache,主要用于读。
        写请求会先写入Memstore,Regionserver会给每个region提供一个Memstore,当Memstore满64MB以后,会启动 flush刷新到磁盘。当Memstore的总大小超过限制时(heapsize * hbase.regionserver.global.memstore.upperLimit * 0.9),会强行启动flush进程,从最大的Memstore开始flush直到低于限制。
        读请求先到Memstore中查数据,查不到就到BlockCache中查,再查不到就会到磁盘上读,并把读的结果放入BlockCache。由于BlockCache采用的是LRU策略,因此BlockCache达到上限(heapsize * hfile.block.cache.size * 0.85)后,会启动淘汰机制,淘汰掉最老的一批数据。
        一个Regionserver上有一个BlockCache和N个Memstore,它们的大小之和不能大于等于heapsize * 0.8,否则HBase不能启动。默认BlockCache为0.2,而Memstore为0.4。对于注重读响应时间的系统,可以将 BlockCache设大些,比如设置BlockCache=0.4,Memstore=0.39,以加大缓存的命中率。
4.HTable和HTablePool
    HTable和HTablePool都是HBase客户端API的一部分,可以使用它们对HBase表进行crud操作。
    HTable是HBase客户端与HBase服务端通讯的Java API对象,客户端可以通过HTable对象与服务端进行CRUD操作(增删改查)。它的创建很简单
        Configuration conf = HBaseConfiguration.create();
        HTable table = new HTable(conf, "tablename");
    HTable使用时的一些注意事项:
        1)规避HTable对象的创建开销
            因为客户端创建HTable对象后,需要进行一系列的操作:检查.META.表确认指定名称的HBase表是否存在,表是否有效等等,整个时间开销比较重,可能会耗时几秒钟之长,因此最好在程序启动时一次性创建完成需要的HTable对象,如果使用Java API,一般来说是在构造函数中进行创建,程序启动后直接重用。
        2)HTable对象不是线程安全的
            HTable对象对于客户端读写数据来说不是线程安全的,因此多线程时,要为每个线程单独创建复用一个HTable对象,不同对象间不要共享HTable对象使用,特别是在客户端auto flash被置为false时,由于存在本地write buffer,可能导致数据不一致。
        3)HTable对象之间共享Configuration
            HTable对象共享Configuration对象,这样的好处在于:
                共享ZooKeeper的连接:每个客户端需要与ZooKeeper建立连接,查询用户的table regions位置,这些信息可以在连接建立后缓存起来共享使用;
                共享公共的资源:客户端需要通过ZooKeeper查找-ROOT-和.META.表,这个需要网络传输开销,客户端缓存这些公共资源后能够减少后续的网络传输开销,加快查找过程速度。
            因此,与以下这种方式相比:
            HTable table1 = new HTable("table1");
            HTable table2 = new HTable("table2");
            下面的方式更有效些:
            Configuration conf = HBaseConfiguration.create();
            HTable table1 = new HTable(conf, "table1");
            HTable table2 = new HTable(conf, "table2");
            即使是高负载的多线程程序,也并没有发现因为共享Configuration而导致的性能问题;如果你的实际情况中不是如此,那么可以尝试不共享Configuration。
    HTablePool
        HTablePool可以解决HTable存在的线程不安全问题,同时通过维护固定数量的HTable对象,能够在程序运行期间复用这些HTable资源对象。
            Configuration conf = HBaseConfiguration.create();
            HTablePool pool = new HTablePool(conf, 10);
        HTablePool可以自动创建HTable对象,而且对客户端来说使用上是完全透明的,可以避免多线程间数据并发修改问题。
        HTablePool中的HTable对象之间是公用Configuration连接的,能够可以减少网络开销。
        HTablePool的使用很简单:每次进行操作前,通过HTablePool的getTable方法取得一个HTable对象,然后进行put/get/scan/delete等操作,最后通过HTablePool的putTable方法将HTable对象放回到HTablePool中。
        下面是个使用HTablePool的简单例子:
        public void createUser(String username, String firstName, String lastName, String email, String password, String roles) throws IOException {
          HTable table = rm.getTable(UserTable.NAME);
          Put put = new Put(Bytes.toBytes(username));
          put.add(UserTable.DATA_FAMILY, UserTable.FIRSTNAME,
          Bytes.toBytes(firstName));
          put.add(UserTable.DATA_FAMILY, UserTable.LASTNAME,Bytes.toBytes(lastName));
          put.add(UserTable.DATA_FAMILY, UserTable.EMAIL, Bytes.toBytes(email));
          put.add(UserTable.DATA_FAMILY, UserTable.CREDENTIALS,Bytes.toBytes(password));
          put.add(UserTable.DATA_FAMILY, UserTable.ROLES, Bytes.toBytes(roles));
          table.put(put);
          table.flushCommits();
          rm.putTable(table);
        }
Hbase和关系型数据库比较        
    hbase对条件查询支持的不是很好,后者可以通过where条件进行
    不支持全文索引。使用solr和hbase整合完成全文搜索。solr可以建立一个数据库(索引文件里面存放第一个是关键字,第二个是这个数据对应的rowkey)从solr可以找到这个单词,那么这个单词在哪个rowkey出现过也可以找到了,最后根据rowkey可以找到hbase表中数据的存放位置
    使用MR批量读取hbase中的数据,在solr里面建立索引(no  store)之保存rowkey的值。
    根据关键词从索引中搜索到rowkey(分页)
    根据rowkey从hbase查询所有数据

    
mr读hbase与mr写hbase
    官网->Documentation and API->Reference Guide->HBase MapReduce Read/Write Example
    注意:mr读hbase通常只有一个map就够了(做etl的时候,根据业务场景自己定方案)
          如果有combiner这样的中间过程,需要MR中的reduce类,因为中间过程的结果还是写到HDFS中,只有最终结果才写到hbase中
          本地运行的时候需要:
          将Hadoop中4个配置放到src中,然后添加conf.set("hbase.zookeeper.quorum","node1,node2,node3");根据该配置会找到hbase
          添加Hadoop的org下的需要修改的类
          链接:https://pan.baidu.com/s/1D7wv25UOYq1tQbju49byJg 提取码:xb5j中HBaseDAOImp.java常用的对表操作的方法都封装好了

转载于:https://www.cnblogs.com/timeTraveler/p/10790154.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值