Hbase写入数据的几种方式

  我们总结一下hbase几种写入常见的方式,以及涉及的应用场景,另外再总结一下其中涉及到的一些原理知识。hbase一般的插入过程都使用HTable对象,将数据封装在Put对象中,Put在new创建的时候需要传入rowkey,并将列族,列名,列值add进去。然后HTable调用put方法,通过rpc请求提交到Regionserver端。写入的方式可以分为以下几种

  1. 单条put
  2. 批量put
  3. bluckload

HTable介绍

   要向hbase中写入就免不了要和HTable打交道,HTable负责向一张hbase表中读或者写数据,HTable对象是非线程安全的。多线程使用时需要注意,创建HTable对象时需要指定表名参数,HTable内部有一个LinkedList<Row>的队列writeAsyncBuffer ,负责对写入到hbase的数据在客户端缓存,开启缓存使用参数  table.setAutoFlushTo(false);  默认情况不开启每次put一条数据时,htable对象就会调用flushCommits方法向regserver中提交,开启缓存则会比较队列的大小,如果大于某个值则调用flushCommits,这个值默认是2m,可以通过在hbase-site.xml中设置参数 "hbase.client.write.buffer"来调整,默认是2097152, 在关闭htable连接时,会隐式的调用flushCommits方法,保证数据完全提交。提交时会根据rowkey定位该put应该提交到哪个reginserver,然后每个regionserver一组action发送出去。

注意:有了BufferedMutator之后,BufferedMutator替换了HTable的setAutoFlush(false)的作用。可以从连接的实例中获取BufferedMutator的实例。在使用完成后需要调用的close()方法关闭连接。对BufferedMutator进行配置需要通过BufferedMutatorParams完成。BufferedMutatorParams要比Htable更搞效,所以心在我们在向hbase插入数据时尽量使用BufferedMutatorParams

单条put

  最简单基础的写入hbase,一般应用场景是线上业务运行时,记录单条插入,如报文记录,处理记录,写入后htable对象即释放。每次提交就是一次rpc请求。

table.setAutoFlushTo(true);
/**
   * 插入一条记录
   * rowkey 为rk001 列族为f1
   * 插入两列  c1列   值为001
   *          c2列   值为002
   *
   */
  public void insertPut(){
      //Configuration 加载hbase的配置信息,HBaseConfiguration.create()是先new Configuration然后调用addResource方法将
      //hbase-site.xml配置文件加载进来
      Configuration conf = HBaseConfiguration.create();
      try {
          table = new HTable(conf,tableName);
          table.setAutoFlushTo(true);//不显示设置则默认是true																														
          String rowkey  = "rk001";
          Put  put = new Put(rowkey.getBytes());
          put.add(cf.getBytes(),"c1".getBytes(),"001".getBytes());
          put.add(cf.getBytes(),"c2".getBytes(),"002".getBytes());
          table.put(put);
          table.close();//关闭hbase连接
 } catch (IOException e) {
          e.printStackTrace();
      }
  }

多条写入  

         有了单条的put自然就想到这种方式其实是低效的,每次只能提交一条记录,有没有上面方法可以一次提交多条记录呢?减少请求次数, 最简单的方式使用List<Put>,这种方式操作时和单条put没有区别,将put对象add到list中,然后调用put(List<Put>)方法,过程和单条put基本一致,应用场景一般在数据量稍多的环境下,通过批量提交减少请求次数

/**
  * 批量请求,一次提交两条
  */

 public void insertPuts() {
     Configuration conf = HBaseConfiguration.create();
     try {
         table = new HTable(conf, tableName);
         table.setAutoFlushTo(true);
         List<Put> lists = new ArrayList<Put>();
         String rowkey1 = "rk001";
         Put put1 = new Put(rowkey1.getBytes());
         put1.add(cf.getBytes(), "c1".getBytes(), "001".getBytes());
         put1.add(cf.getBytes(), "c2".getBytes(), "002".getBytes());
         lists.add(put1);
         String rowkey2 = "rk002";
         Put put2 = new Put(rowkey2.getBytes());
         put2.add(cf.getBytes(), "c1".getBytes(), "v2001".getBytes());
         put2.add(cf.getBytes(), "c2".getBytes(), "v2002".getBytes());
         lists.add(put2);
         table.put(lists);
         table.close();
     } catch (IOException e) {
         e.printStackTrace();
     }
 }

BufferedMutatorParams的使用

             org.apache.hadoop.hbase.client.HTable归根结底持有的就是BufferedMutatorImpl类型的属性mutator,所有后续的操作都是基于mutator操作, 那么其实我们操作hbase客户端,完全可以摒弃HTable对象,直接构建BufferedMutator,然后操作hbase,BufferedMutatorParams主要是收集构造BufferedMutator对象的参数信息,这些参数包括hbase数据表名、hbase客户端缓冲区、hbase rowkey最大所占空间、线程池和监听hbase操作的回调监听器(比如监听hbase写入失败)。

使用方式:

正如BufferedMutatorParams需要参数一样,我们需要提供表名,设置好缓存的大小,初始化mutator实例然后提价put对应,向hbase插入数据

案例:

//一个Put对象就是一行记录,在构造方法中指定主键
      val put = new Put(Bytes.toBytes(MD5Util.getMD5(userId + userName)))
      put.addColumn(Bytes.toBytes("hiveData"), Bytes.toBytes("id"), Bytes.toBytes(userId)).addColumn(Bytes.toBytes("hiveData"), Bytes.toBytes("name"), Bytes.toBytes(userName)) .addColumn(Bytes.toBytes("hiveData"), Bytes.toBytes("money"), Bytes.toBytes(userMoney))
      putList.add(put)
//设置缓存1m,当达到1m时数据会自动刷到hbase
      val params = new BufferedMutatorParams(TableName.valueOf("test6"))
      params.writeBufferSize(1024 * 1024) //设置缓存的大小
      val mutator = connection.getBufferedMutator(params)
      mutator.mutate(putList)
      mutator.flush()
      putList.clear()

hbase操作的回调监听器

      BufferedMutator是通过设定BufferedMutator.ExceptionListener监听器来异步处理异常,重写onException来实现异常处理,该监听器用来监听接受服务器端发送回来的错误消息,官方案例如下:

public class  extends Configured implements Tool {
  private static final Logger LOG = LoggerFactory.getLogger(BufferedMutatorExample.class); 
  private static final int POOL_SIZE = 10;
  private static final int TASK_COUNT = 100;
  private static final TableName TABLE = TableName.valueOf("tanle_name");
  private static final byte[] FAMILY = Bytes.toBytes("f");
  public int run(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
    //异常监听器的建立,当写入异常是触发该监听器
    final BufferedMutator.ExceptionListener listener = new BufferedMutator.ExceptionListener() {
      public void onException(RetriesExhaustedWithDetailsException e, BufferedMutator mutator) {
        for (int i = 0; i < e.getNumExceptions(); i++) {
          LOG.info("Failed to sent put " + e.getRow(i) + ".");
        }
      }
    };
    //创建BufferedMutatorParams对象,设置监听器
    BufferedMutatorParams params = new BufferedMutatorParams(TABLE)
        .listener(listener);
    //可以改动缓冲区的大小,如下面设置缓冲区大小为4M
    params.writeBufferSize(4*1023*1024);
    try (final Connection conn = ConnectionFactory.createConnection(getConf());
         final BufferedMutator mutator = conn.getBufferedMutator(params)) {
      final AtomicInteger count = new AtomicInteger(1);
      final ExecutorService workerPool = Executors.newFixedThreadPool(POOL_SIZE);
      List<Future<Void>> futures = new ArrayList<>(TASK_COUNT);
      for (int i = 0; i < TASK_COUNT; i++) {
        futures.add(workerPool.submit(new Callable<Void>() {
          public Void call() throws Exception {
            Integer value = count.getAndIncrement();
            Put p = new Put(Bytes.toBytes("task " + value));
            p.addColumn(FAMILY, Bytes.toBytes("someQualifier"), Bytes.toBytes("task " + value + " info"));
            mutator.mutate(p);
            return null;
          }
        }));
      }
      for (Future<Void> f : futures) {
        f.get(5, TimeUnit.MINUTES);
      }
      workerPool.shutdown();
    } catch (IOException e) {
      LOG.info("exception while creating/destroying Connection or BufferedMutator", e);
    } 
    return 0;
  }
  public static void main(String[] args) throws Exception {
    ToolRunner.run(new BufferedMutatorExample(), args);
  }
}

上面的官方例子就是启动线程不断提交数据,BufferedMutator中缓冲区可以避免频繁的调用RPC,在批处理数据时及其重要,并且BufferedMutator的mutate操作是异步的,所以不会产生阻塞,这在Map-Reduce作业有很好的使用,BufferedMutator接收来自MR作业的puts,异步的批量提交数据,不影响MR作业的运行。

错误处理

当通过BufferedMutator批量提交发生错误时触发绑定的BufferedMutator.ExceptionListener监听器实例的onException方法,其中RetriesExhaustedWithDetailsException记录了发生错误的内容及其提交的错误内容等信息,而其余正确的提交的内容则会正确放入HBase表中。

对于提交内容的检查分为客户端的检查和服务器端的检查。

  • 当客户端检查到提交的内容出错(比如Put未添加内容或者未指定列),会抛出客户端的错误,这样错误不会RetriesExhaustedWithDetailsException监听器接受,被其运行的线程会因错误而终止,则在该Put之后的内容都不会提交。
  • 当服务端检查到提交的内容出错(比如指定的列簇不存在),会向客户端传输错误,而这错误会被RetriesExhaustedWithDetailsException监听器接受,不会对后续提交的数据产生影响。

Table的put,get,delete方法提交一个(put,get,delete)列表操作,其中有错误的内容时,也分客户端的检查和服务器端的检查(不同操作检查内容不同),在客户端检查出错会抛出异常终止程序,服务端异常时会传输会错误信息,但是其余正确的操作已将提交到服务端并被正确执行。

获取数据Get用法

简介

HTable类中提供了get()方法,同时还有与之对应的Get类。get方法分为两类:

  1. 一类是一次获取一行数据
  2. 另一类是一次获取多行数据

单行get

这种方法可以从HBase中获取一个特定的值:

Result get(Get get) throws IOException

与put()方法对应Put类相似,get()方法也有对应的Get类,此外还有一个相似之处,那就是在使用下面的方法构造Get实例时,与也需要设置行键:

Get(byte [] row)
Get(byte [] row,RowLock rowLock)

虽然一次get()操作只能读取一行数据,但不会限制一行当中取多少列或者多少单元格。 这两个Get实例都通过row参数指定了要获取的行,其中第二个Get实例还增加了一个可选的rowLock参数,允许用户设置行锁。 与put操作一样,用户有许多方法可用,可以通过多种标准筛选目标数据,也可以指定精确的坐标获取某个单元格的数据:

Get addFamily(byte [] family)                   // 限制get请求只能取得一个制定的列族,要取得多个列族时需要多次调用。
Get addColumn(byte [] family,byte [] qualifier) // 用户通过他可以指定get取得那一列的数据,从而进一步缩小地址空间。
Get setTimeRange(long minStamp,long maxStamp)   // 设置只能在制定的时间戳范围获得列版本。
Get setTimeStamp(long timestamp)                // 获得带有制定时间戳的列的版本。
Get setMaxVersions()                            // 设置最大版本数,设置为最大版本数为Integer的最大值。
Get setMaxVersions(int maxVersions)

说明:

         addFamily()方法限制get请求只能取得一个指定的列族,要取得多次调用。addColumn()方法的使用与之类似,用户通过它可以指定get取得哪一列的数据,从而进一步缩小地址空间。有一些方法允许用户设定要获取的数据的时间戳,或者通过设定一个时间段来取得时间戳属于该时间段内的数据。 如果用户没有设定时间戳的话,也有方法允许用户设定要获取的数据的版本数目。默认情况下,版本数为1,即get()方法允许用户设定要获取的数据的版本数目。默认情况下,版本数为1,即get()请求返回最新的匹配版本。如果有疑问,可以使用getMaxVersions()来检查这个Get实例所要获取的最大版本数。不带参数的setMaxVersions()方法会把要取出的最大版本数设为Integer.MAX_VALUE,这是用户在列族描述符中可配置的最大版本数,此时系统会返回这个单元格中所有的版本,换句话说,此时系统会返回用户在列族中已配置的最大版本数以内的所有数据。

方法

描述

getRow()

返回创建Get实例时指定的行键

getRowLock()

返回当前Get实例的RowLock实例

getLockId()

返回创建时指定rowLock的锁ID,如果没有指定返回-1L

getTimeRange()

返回指定的Get实例的时间戳范围。注意,Get类中已经没有getTimeStamp()方法了,因为API会在内部将setTimeStamp()赋的值转换成TimeRange实例,设定给定时间戳的最大值和最小值

setFilter()/getFilter()

用户可以使用一个特定的过滤器实例,通过多种规则和条件来筛选列和单元格。使用这些方法用户可以设定或查看Get实例的过滤器成员

setCacheBlocks()/getCacheBlocks()

每个HBase的region服务器都有一个块缓存来有效地保存最近存取过的数据,并以此来加速之后的相邻信息的读取。不过在某些情况下,例如完全随机读取时,最好能避免这种机制带来的扰动。这些方法能够控制当次读取的块缓存机制是否启效

numFamilies()

快捷地获取列族FamilyMap大小的方法,包括用addFamily()方法和addColumn()方法添加的列族

hasFamilies()

检查列族或列是否存在于当前的Get实例中

familySet()/getFamilyMap()

这些方法能让用户直接访问addFamily()和addColumn()添加的列族和列。FamilyMap列族中键是列族的名称,键对应的值是指定列族的限定符列表。familySet()方法返回一个所有已存储列族的Set,即一个只包含列族名的集合

下边代码展示了从HBase中获取数据的完整过程。

Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "testtable");
Get get = new Get(Bytes.toBytes("row1"));
get.addColumn(Bytes.toBytes("colfam1"), Bytes.toBytes("qual1"));
Result result = table.get(get);
byte[] val = result.getValue(Bytes.toBytes("colfam1"),Bytes.toBytes("qual1"));
System.out.println("Values:" + Bytes.toString(val));

Result实例

             当用户使用get()方法获取数据时,HBase返回的结果包含所有匹配的单元格数据,这些数据将被封装在一个Result实例中返回给用户。用它提供的方法,可以从服务器端获取匹配指定行的特定返回值,这些值包括列族、列限定符和时间戳等。提供的方法如下

byte[] getValue(byte[] family,byte[] qualifier) // 方法允许用户取得一个HBase中存储的特定单元格的值。因为该方法不能指定时间戳,所以用户只能获取数据最新的版本。
byte [] value()                                 // 返回第一个列对应的最新单元格的值。
byte [] getRow()                                // 返回创建Get对象当前实例中的rowKey
int size()                                      // 返回Cell实例的数目
boolean isEmpty()                               // 查看键值对的数目是否大于0
Cell[] rawCells()                               // 返回的数组已经按照字典序排列;排序规则是先按列族排序,列族内再按列限定符排序,此后再按时间戳排序,最后按类型排序。
List<Cell> listCells()                          // 返回所有Cell对象的集合
List<Cell> getColumnCells(byte [] family, byte [] qualifier)  // 返回特定列的单元格
Cell getColumnLatestCell(byte [] family, byte [] qualifier)   // 返回最新版本的Cell对象
boolean containsColumn(byte [] family, byte [] qualifier)     // 检查指定列的值是否存在

getValue()方法:

允许用户取得一个HBase中存储的特定单元格的值。因为该方法不能指定时间戳,所以用户只能获取数据最新的版本。

value()方法:

的使用更简单,它会返回第一个列对应的最新单元格的值。因为列在服务器端是按字典存储的,所以会返回名称(包括列族和列限定符)排在首位的那一列的值。

getRow()方法:

它返回创建Get类当前实例使用的行键。size()方法返回服务器端返回值中键值对(KeyValue实例)的数目。用户可以使用size()方法或者isEmpty()方法查看键值对的数目是否大于0,这样可以检查服务器端是否找到了与查询相对应的结果。

raw()方法:

返回原始的底层KeyValue的数据结构。具体来说,是基于当前的Result实例返回KeyValue实例的数组。

list():

调用则把raw()中返回的数组转化为一个list()实例,并返回给用户,创建的List实例由原始返回结果中的KeyValue数组成员组成,用户可以方便地地带使用数据。 raw()方法返回的数组已经按照字典序排列,排列时考虑了KeyValue实例的所有坐标。先按列族排序,列族内再按列限定符排序,此后再按时间戳排序,最后按类型排序

另外还有一些面向列的存取函数如下:

list<KeyVlue> getColumn(byte[] family,byte[] qualifier)
//获取特定列的多个版本,版本数取决于调用get前,创建get时设置的最大版本数
KeyVlue getColumnLatest(byte[] family,byte[] qualifier)
//返回对于列最新值的KeyVlue ,在不只是需要数据时很有用
boolean containsColumn(byte[] family,byte[] qualifier)
//返回是否有对于列
//这些方法也可以不知道列限定符,将列限定符设置为null。这样会匹配 没有列限定符的特殊列

返回值中的版本数取决于用户调用get()方法之前,创建Get()实例时设置的最大版本数,默认是1。换句话说,getColum()返回的列表中包括0(当本行没有该列值时)或1个条目,这一条目时该列最新版本的值。如果用户指定了一个比默认值1大的版本数,返回的列表中就可能会有多个条目。 getColumnLatest()方法返回对应列的最新版本值,不过与getValue()不同,它不返回值的原始字节数组,而是返回一个KeyValue实例。如果用户需要的不仅仅是数据,那么这个方法将会非常有用。containsColumn()检查返回值中是否有对应的列。不使用限定符就意味着这一列没有标签。当查询表时,例如,用户通过命令行查询时,需要自己明白数据所表示的具体含义。可能只有一种情况能用到空限定符:即一个列族下只有一列,同时列族名就能够表示数据的含义及目的。

下面试第三类取值函数,以映射形式返回结果:

NavigableMap<byte [], NavigableMap<byte[], NavigableMap<Long, byte[]>>>getMap()
NavigableMap<byte[], byte[] >> getNoVersionMap()
NavigableMap<byte[], byte[]> getFamilyMap(byte[] family)

最常用的方法是getMap(),它把所有get()请求返回的内容都装入一个Java的Map类实例,这样用户可以使用该方法遍历所有结果。getNoVersionMap()与getMap()形式上相似,不过它只返回每个列的最新版本。getFamilyMap()允许用户指定一个特定的列族,返回这次结果中这个列族下的所有版本。

不论用户使用什么方法获取Result中的数据,都不会产生额外的性能和资源消耗,因为这些数据都已经通过网络从服务端传输到了客户端。

多条Get

          使用列表参数的get()方法与使用列表参数的put()方法对应,用户可以用一次请求获取多行数据。它允许用户快速高效的从远程服务器获取相关的或完全随机的多行数据,使用时把多个get请求封装到list<Get>中一起请求。
API提供的方法签名如下:

Result[] get(List<Get> gets)throws IOException

注意:get()方法要么返回与给定列表大小一致的Result数组,要么抛出一个异常

Configuration conf = HBaseConfiguration.create();
HTable table = new HTable(conf, "tableinfo");
byte[] cf1 = Bytes.toBytes("colfam1");
byte[] qf1 = Bytes.toBytes("qual1");
byte[] qf2 = Bytes.toBytes("qual2");
byte[] row1 = Bytes.toBytes("row1");
byte[] row2 = Bytes.toBytes("row2");
List<Get> gets = new ArrayList<Get>();
Get get1 = new Get(row1);
get1.addColumn(cf1, qf1);
gets.add(get1);
Get get2 = new Get(row2);
get2.addColumn(cf1, qf2);
gets.add(get2);
Result[] results = table.get(gets);
for (Result result : results)
{
    String row = Bytes.toString(result.getRow());
    System.out.println("Row:" + row + " ");
    byte[] val = null;
    if (result.containsColumn(cf1, qf1))
    {
        val = result.getValue(cf1, qf1);
        System.out.println("Value:" + Bytes.toString(val));
    }
    if (result.containsColumn(cf1, qf2))
    {
        val = result.getValue(cf1, qf2);
        System.out.println("Value:" + Bytes.toString(val));
    }
}
}

HbaseUtil

package com.techwolf.flink.learning.application.utils;

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 java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class HbaseUtil {
    public static Configuration conf       = null;
    public static Connection    connection = null;
    public static Admin         admin      = null;

    /**
     * @desc 取得连接
     */
    public  void setConf(String quorum, String port) {
        try {
            conf = HBaseConfiguration.create();
            conf.set("hbase.zookeeper.quorum", quorum);//zookeeper地址
            conf.set("hbase.zookeeper.property.clientPort", port);
            connection = ConnectionFactory.createConnection(conf);
            admin = connection.getAdmin();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * @desc 连关闭接
     */
    public void close() {
        try {
            if (connection != null) {
                connection.close();
            }
            if (admin != null) {
                admin.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * @desc 创建表
     */
    public  void createTable(String tableName, String columnFamily) {


        try {
            TableName tbName = TableName.valueOf(tableName);
            if (!admin.tableExists(tbName)) {
                HTableDescriptor  hTableDescriptor  = new HTableDescriptor(tbName);
                HColumnDescriptor hColumnDescriptor = new HColumnDescriptor(columnFamily);
                hTableDescriptor.addFamily(hColumnDescriptor);
                admin.createTable(hTableDescriptor);
            }
        } catch (IOException e) {
            e.printStackTrace();

        }


    }


    /**
     * @desc 查询rowkey下某一列值
     */
    public  String getValue(String tableName, String rowKey, String family, String qualifier) {
        Table table = null;
        try {
            table = connection.getTable(TableName.valueOf(tableName));
            Get get = new Get(rowKey.getBytes());
            //返回指定列族、列名,避免rowKey下所有数据
            get.addColumn(family.getBytes(), qualifier.getBytes());
            Result rs = table.get(get);
            // 返回最新版本的Cell对象
            Cell   cell  = rs.getColumnLatestCell(family.getBytes(), qualifier.getBytes());
            String value = null;
            if (cell!=null) {
                value = Bytes.toString(CellUtil.cloneValue(cell));
            }
            return value;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        } finally {
            if (table!=null){
                try {
                    table.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }

    }
    /**
     * 添加多条记录
     */
    public void addMoreRecord(String tableName, String family, String  qualifier, List < String > rowList, String value){
        Table table = null;
        try {
            table = connection.getTable(TableName.valueOf(tableName));

            List<Put> puts = new ArrayList<>();
            Put       put  = null;
            for (int i = 0; i < rowList.size(); i++) {
                put = new Put(Bytes.toBytes(rowList.get(i)));
                put.addColumn(Bytes.toBytes(family), Bytes.toBytes(qualifier), Bytes.toBytes(value));

                puts.add(put);
            }
            table.put(puts);

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (table != null) {
                try {
                    table.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //判断表是否存在
    public boolean isTableExist(String tableName) throws IOException {
        TableName tbName = null;
        try {
            tbName = TableName.valueOf(tableName);
            boolean flog=admin.tableExists(tbName);
            return  flog ;
        } catch (Exception e) {
            e.printStackTrace();
            if (connection != null) {
                connection.close();
            }
            if (admin != null) {
                admin.close();
            }
            return false;
        }


    }

}