我们之后的实际开发中不可能在服务器那边直接使用shell命令一直敲的,一般都是通过API进行操作的。
环境准备
新建Maven项目,导入Maven依赖
<dependencies> <dependency> <groupId>org.apache.hbase</groupId> <artifactId>hbase-client</artifactId> <version>2.4.17</version> </dependency> </dependencies>
1、创建连接
HBase 的客户端连接由 ConnectionFactory 类来创建(工厂模式直接创建),我们使用完之后需要手动关闭连接。同时连接 是一个重量级的,推荐一个进程使用一个连接,对 HBase 的命令通过连接中的两个属性 Admin 和 Table 来实现。其中 Admin 主要是针对元数据-表格的创建修改进行操作, Table 则是针对表格中数据的增加修改进行操作。
1.1、单线程创建连接
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.client.AsyncConnection; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.CompletableFuture; public class HBaseConnection { public static void main(String[] args) throws IOException { // 1. 创建连接配置参数 Configuration conf = new Configuration(); //对应我们 hbase-site.xml 中的配置信息的<name>和<value>的值 conf.set("hbase.zookeeper.quorum","hadoop102,hadoop103,hadoop104"); // 2. 创建连接 // 默认使用同步连接 Connection connection = ConnectionFactory.createConnection(conf); // 3. 使用连接 System.out.println(connection); // 4. 关闭连接 connection.close(); } }
1.2、多线程创建连接
我们真正开发中首先不会把配置参数写到代码中的,我们是通过Maven项目下的resources目录来读取配置文件来设置配置参数的,我们可以看源码:
Connection connection = ConnectionFactory.createConnection();
我们调用了工厂模式的 ConnectionFactory 的 createConnection 方法来创建连接,这里我们。
没有配置参数,因为HBase默认其实会自动帮我们添加配置参数:
我们可以看到当调用ConnectionFactory 的 createConnection 方法的时候,其实又调用了HBaseConfiguration 的 create 方法,
该方法内部帮我们添加了配置参数:
可以看到,它其实是去读取我们Maven项目下的resources目录下的文件,所以我们需要将我们的配置参数写到resources目录下,最好使用 "hbase-ste.xml" 来命名,至于这个文件,我们直接复制我们hbase集群中conf目录下的hbase-site.xml 。
其中,我们只需要留下关于我们zookeeper服务器连接地址的配置信息即可,别的全部删除,因为我们是客户端,我们不能设置服务端的配置,那些即使写了也不会生效。
<?xml version="1.0"?> <?xml-stylesheet type="text/xsl" href="configuration.xsl"?> <configuration> <property> <name>hbase.zookeeper.quorum</name> <value>hadoop102,hadoop103,hadoop104</value> <description>The directory shared by RegionServers.</description> </property> </configuration>
使用类单例模式确保只使用一个连接,可以同时用于多个线程。
import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.client.AsyncConnection; import org.apache.hadoop.hbase.client.Connection; import org.apache.hadoop.hbase.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.CompletableFuture; public class HBaseConnection { // 声明一个静态属性 public static Connection connection = null; static { // 1. 创建连接 // 默认使用同步连接 try { //使用读取本地配置文件的方式来添加参数 connection = ConnectionFactory.createConnection(); } catch (IOException e) { e.printStackTrace(); } } public static void closeConnection() throws IOException { // 判断连接是否为 null if (connection != null){ connection.close(); } } public static void main(String[] args) throws IOException { //使用多线程连接 直接使用创建好的连接 不再main线程单独创建 System.out.println(HBaseConnection.connection); //在main线程的最后记得关闭连接 HBaseConnection.closeConnection(); } }
2、DDL
创建 HBaseDDL类,添加HBaseConnection的静态属性作为我们的连接对象,确保单例模式。
2.1、创建命名空间
我们上面说了,HBase中的 DDL 语句被封装到了 Admin中,所以我们需要先获取 Admin。
Admin admin = connection.getAdmin();
注意:在coding的过程中遇到异常不要老想着直接在方法名之后直接 throws ,这样虽然是简洁了一些,但是如果第一行抛出了一个IOException,之后几行再出现异常我们就察觉不到了,所以尽量在我们核心代码处try-catch,方便了解异常信息。
然后我们直接通过方法来创建 namespace ,这里的namespace是一个对象,这样做的原因是因为我们 HBase 的shell命令中创建namespace的时候就是不止一种方法,所以这里单纯字符串来创建namespace肯定不行,对象具有更完整属性。
第二种创建命名空间的方式中,我们可以看到有一个 键值对参数,这就需要设置我们对象的属性了。
import org.apache.hadoop.hbase.NamespaceDescriptor; import org.apache.hadoop.hbase.client.Admin; import org.apache.hadoop.hbase.client.Connection; import java.io.IOException; public class HBaseDDL { public static Connection connection = HBaseConnection.connection; /** * 创建命名空间 * @param namespace 命名空间的名称 */ public static void createNamespace(String namespace) throws IOException { // 1. 获取admin //admin是轻量级的 并且不是线程安全的 不推荐池化或者缓存这个连接 //也就是说 用的时候再去获取 不用就把它关闭掉 Admin admin = connection.getAdmin(); // 2. 调用方法创建 namespace // 代码比shell更加底层 所以shell能实现的功能代码 一定也可以 // 所以代码实现时 需要更完整的命名空间描述 // 2.1 获取一个命名空间的建造者 => 设计师 NamespaceDescriptor.Builder builder = NamespaceDescriptor.create(namespace); // 2.2 给命名空间添加属性 // 给namespace添加键值对属性其实并没有什么意义 只是给人注释一样 builder.addConfiguration("user","lyh"); // 2.3 使用builder构造出namespace对象 // 创建命名空间造成的问题 属于方法本身的问题 不应该抛出 try { admin.createNamespace(builder.build()); } catch (IOException e) { System.out.println("该命名空间已经存在!"); e.printStackTrace(); } // 3. 关闭资源 admin.close(); } public static void main(String[] args) throws IOException { //测试创建马命名空间 createNamespace("lyh"); //记得关闭HBase连接 HBaseConnection.closeConnection(); } }
运行结果
2.2、判断表格是否存在
/** * 判断表格是否存在 * @param namespace 命名空间 * @param tableName 表名 * @return true-存在 false-不存在 */ public static boolean isTableExists(String namespace,String tableName) throws IOException { // 1. 获取admin Admin admin = connection.getAdmin(); // 2. 使用方法判断表格是否存在 boolean b = false; try { b = admin.tableExists(TableName.valueOf(namespace, tableName)); } catch (IOException e) { e.printStackTrace(); } // 3. 关闭admin admin.close(); // 4.返回结果 return b; }
2.3、创建表
/** * 创建表格 * @param namespace 命名空间 * @param tableName 表格名称 * @param columnFamilies 列族名称 可以有多个 */ public static void createTable(String namespace,String tableName,String... columnFamilies) throws IOException { // 判断是否有至少一个列族 if (columnFamilies.length == 0){ System.out.println("创建表格至少应该有一个列族"); return; } // 判断表格是否已经存在 if (isTableExists(namespace,tableName)){ System.out.println("表格已经存在"); return; } // 1. 获取admin Admin admin = connection.getAdmin(); // 2. 调用方法创建表格 // 2.1 获取表格的建造者 TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(TableName.valueOf(namespace,tableName)); // 2.2 添加参数 for (String columnFamily : columnFamilies) { // 2.3 获取列族建造者 ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily)); // 2.4 通过建造者创建对应列族描述 // 添加版本参数-维护的版本数 columnFamilyDescriptorBuilder.setMaxVersions(5); // 2.5 创建添加完参数的列族描述 builder.setColumnFamily(columnFamilyDescriptorBuilder.build()); } // 2.3 创建表格描述 try { admin.createTable(builder.build()); } catch (IOException e) { //System.out.println("表格已经存在"); e.printStackTrace(); } // 2.4 关闭admin admin.close(); }
2.4、修改表
这里需要注意的比较多:
我们这里修改表格的列族版本,首先就需要获取表格描述和列族描述,但是我们不能重新通过newBuilder创建这两种描述,而是应该使用旧的描述。
对于旧的表格描述来说,我们可以通过admin的getDescriptor()来获取旧的描述。
对于旧的列族描述来说,我们可以通过表格描述对象的getColumnFamily()方法来获取。
/** * 修改表格中一个列族的版本 * @param namespace 命名空间 * @param tableName 表名 * @param columnFamily 列族 * @param version 维护的版本 */ public static void modifyTable(String namespace,String tableName,String columnFamily,int version) throws IOException { // 判断表格是否存在 if (!isTableExists(namespace,tableName)){ System.out.println("表格不存在"); return; } // 1. 获取admin Admin admin = connection.getAdmin(); // 2. 调用方法修改表格 // 2.0 获取之前的表格描述 TableDescriptor tableDescriptor = null; try { tableDescriptor = admin.getDescriptor(TableName.valueOf(namespace, tableName)); } catch (IOException e) { System.out.println("表格不存在"); e.printStackTrace(); } // 2.1 创建一个表格描述建造者 // 如果使用填写 tableName 的方法 相当于创建了一个新的表格描述 没有之前的信息 // 如果想要修改表格的信息 必须调用方法填写一个旧的表格描述 TableDescriptorBuilder builder = TableDescriptorBuilder.newBuilder(tableDescriptor); // 2.2 对应建造者进行表格数据的修改 // 获取旧的列族描述 ColumnFamilyDescriptor columnFamily1 = tableDescriptor.getColumnFamily(Bytes.toBytes(columnFamily)); // 创建列族描述建造者 ColumnFamilyDescriptorBuilder columnFamilyDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(columnFamily1); // 修改对应的版本 columnFamilyDescriptorBuilder.setMaxVersions(version); // 在这里修改的时候 如果填写的是新创建的列族描述 那么我们表格之前的其它属性会被初始化 所以要使用旧的列族描述 builder.modifyColumnFamily(columnFamilyDescriptorBuilder.build()); try { admin.modifyTable(builder.build()); } catch (IOException e) { e.printStackTrace(); } // 3. 关闭admin admin.close(); }
2.5、删除表
需要注意HBase中删除表前必须标记表为不可用!
/** * 删除表格 * @param namespace 命名空间 * @param tableName 表名 * @return true-删除成功 */ public static boolean deleteTable(String namespace,String tableName) throws IOException { // 1. 判断表格是否存在 if (!isTableExists(namespace,tableName)){ System.out.println("表格不存在 无法删除"); return false; } // 2. 获取admin Admin admin = connection.getAdmin(); // 3. 调用相关的方法删除表格 try { // hbase 删除表格前必须标记标记表格为不可用才能删除 admin.disableTable(TableName.valueOf(namespace,tableName)); admin.deleteTable(TableName.valueOf(namespace,tableName)); } catch (IOException e) { e.printStackTrace(); } // 4. 关闭admin admin.close(); return true; }
3、DML
3.1、插入数据
我们可以看到,插入数据的put方法中要求参数必须为Byte类型,这也应证了我们之前第一篇博客说的-HBase的Cell的数据都是以Byte字节类型存储的。
public class HBaseDML { //静态属性 public static Connection connection = HBaseConnection.connection; /** * 插入数据 * @param namespace 命名空间 * @param tableName 表名 * @param rowKey 行键 * @param columnFamily 列族 * @param columnName 列名 * @param value 值 */ public static void putCell(String namespace,String tableName,String rowKey,String columnFamily,String columnName,String value) throws IOException { // 1. 获取 Table Table table = connection.getTable(TableName.valueOf(namespace,tableName)); // 2. 调用相关方法实现数据插入 // 2.1 创建 put 对象 Put put = new Put(Bytes.toBytes(rowKey)); // 2.2 给 put 对象添加属性 put.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(columnName),Bytes.toBytes(value)); // 2.3 将对象写入对应的方法 try { table.put(put); } catch (IOException e) { e.printStackTrace(); } // 3. 关闭table table.close(); } public static void main(String[] args) throws IOException { // 测试插入数据 putCell("bigdata","student","1005","info","name","hbase"); System.out.println("其他代码"); // 关闭连接 HBaseConnection.closeConnection(); } }
3.2、查询数据
/** 读取数据 读取对应的一行中的某一列 * @param namespace 命名空间 * @param tableName 表名 * @param rowKey 行键 * @param columnFamily 列族 * @param columnName 列名 * @return 返回最小单位集合 Cells */ public static Cell[] getCells(String namespace,String tableName,String rowKey,String columnFamily,String columnName) throws IOException { // 1. 获取Table Table table = connection.getTable(TableName.valueOf(namespace,tableName)); // 2. 创建get对象 Get get = new Get(Bytes.toBytes(rowKey)); // 3. 读取数据 // 如果直接调用get方法读取数据 读到的是一整行数据 // 如果想读取某一列的数据 需要添加对应的参数 get.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(columnName)); // 设置读取数据的版本所有版本 get.readAllVersions(); // 读取数据 得到result对象 Result result = null; try { result = table.get(get); } catch (IOException e) { e.printStackTrace(); }finally { table.close(); } //返回结果 if (result!=null) return result.rawCells(); else return null; } /** * 打印Cell的值 * @param cells Cell数组 */ public static void printCells(Cell[] cells){ for (Cell cell : cells) { // cell 存储数据比较底层 String rowKey = new String((CellUtil.cloneRow(cell))); String columnFamily = new String(CellUtil.cloneFamily(cell)); String columnName = new String(CellUtil.cloneQualifier(cell)); String value = new String(CellUtil.cloneValue(cell)); System.out.print(rowKey + "-" + columnFamily + "-" + columnName + "-" + value + "\t"); } }
3.3、扫描数据
我们打印的范围是 [startRow,stopRow) 的,是不包含 stopRow 的,如果我们要打印出最后一位的话,stopRow+1 越界也是没有问题的,比如下面我们一共有5行数据,我们设置终止行键为 6 就是没问题的不会报错。
/** * 打印Cell的值 * @param cells Cell数组 */ public static void printCells(Cell[] cells){ for (Cell cell : cells) { // cell 存储数据比较底层 String rowKey = new String((CellUtil.cloneRow(cell))); String columnFamily = new String(CellUtil.cloneFamily(cell)); String columnName = new String(CellUtil.cloneQualifier(cell)); String value = new String(CellUtil.cloneValue(cell)); System.out.print(rowKey + "-" + columnFamily + "-" + columnName + "-" + value + "\t"); } } /** * 扫描数据 [起始行健,终止行键) * @param namespace 命名空间 * @param tableName 表名 * @param startRow 起始行健 * @param stopRow 终止行键 */ public static void scan(String namespace,String tableName,String startRow,String stopRow) throws IOException { // 1. 获取table Table table = connection.getTable(TableName.valueOf(namespace, tableName)); // 2. 创建Scan对象 Scan scan = new Scan(); // 添加参数 来限制扫描的范围 否则扫描整张表 scan.withStartRow(Bytes.toBytes(startRow)); scan.withStopRow(Bytes.toBytes(stopRow)); try { // 读取多行数据 获得scanner ResultScanner scanner = table.getScanner(scan); // result 来记录一行数据 本质是一个 Cell 数组 // resultScanner 来记录多行数据 本质是一个 result 数组 for (Result result : scanner) { printCells(result.rawCells()); System.out.println(); //打印完一个行键对应的行后换行 } } catch (IOException e) { e.printStackTrace(); } // 3. 关闭table table.close(); }
3.4、带过率扫描
带过滤扫描不仅可以扫描最新的数据,还可以扫描到该列族维护的最大版本范围内的历史数据。
/** * 单列带过滤扫描数据 [起始行健,终止行键) * @param namespace 命名空间 * @param tableName 表名 * @param startRow 起始行键 * @param stopRow 终止行键 * @param columnFamily 列族 * @param columnName 列名 * @param value value值 * @throws IOException IO异常 */ public static void filterScan(String namespace,String tableName,String startRow,String stopRow,String columnFamily,String columnName,String value) throws IOException { // 1. 获取table Table table = connection.getTable(TableName.valueOf(namespace, tableName)); // 2. 创建Scan对象 Scan scan = new Scan(); // 添加参数 来限制扫描的范围 否则扫描整张表 scan.withStartRow(Bytes.toBytes(startRow)); scan.withStopRow(Bytes.toBytes(stopRow)); // 可以添加多个过滤 FilterList filterList = new FilterList(); // 创建过滤器 // (1) 结果只保留当前列的数据 ColumnValueFilter columnValueFilter = new ColumnValueFilter( Bytes.toBytes(columnFamily), Bytes.toBytes(columnName), // 比较关系 CompareOperator.EQUAL, Bytes.toBytes(value)); filterList.addFilter(columnValueFilter); // 添加过滤 scan.setFilter(filterList); try { // 读取多行数据 获得scanner ResultScanner scanner = table.getScanner(scan); // result 来记录一行数据 本质是一个 Cell 数组 // resultScanner 来记录多行数据 本质是一个 result 数组 for (Result result : scanner) { printCells(result.rawCells()); System.out.println(); //打印完一个行键对应的行后换行 } } catch (IOException e) { e.printStackTrace(); } // 3. 关闭table table.close(); }
整行过滤就是结果集不只是单单我们搜索的那一列数据而是整行数据(是整行数据,包括所有列族所有列,而不是一个Cell),需要使用 SingleColumnVlaueFilter 就好,需要注意的是,如果一行数据的某一列为空,而那一列的值恰好是我们过滤的关键字,那么这一行数据也会被添加到结果集。
// (2) 结果保留整行数据 包含其他列 // 结果会保留没有当前列的数据 比如我们要info:name=张三的整行数据 但是有这么一行数据的info列族下name是空的 这种数据也会被获取到结果集中去 SingleColumnValueFilter singleColumnValueFilter = new SingleColumnValueFilter( Bytes.toBytes(columnFamily), Bytes.toBytes(columnName), CompareOperator.EQUAL, Bytes.toBytes(value) ); filterList.addFilter(singleColumnValueFilter);
3.5、删除数据
HBase使用版本控制来管理数据的多个版本,当最新版本的数据被删除后,HBase会使用次新版本的数据填充原来的位置。所以如果一个Cell的某一列有多个版本,当我们仅仅删除最新版之后,会有旧版本来填充被删除的位置。
/** * 删除一行中的一列数据 * @param namespace 命名空间 * @param tableName 表格名称 * @param rowKey 行键 * @param columnFamily 列族 * @param columnName 列名 */ public static void deleteColumn(String namespace,String tableName,String rowKey,String columnFamily,String columnName) throws IOException { // 1. 获取table Table table = connection.getTable(TableName.valueOf(namespace,tableName)); // 2. 创建delete对象 Delete delete = new Delete(Bytes.toBytes(rowKey)); // 添加列信息 // addColumn 删除最新版本 // addColumns 删除所有版本 delete.addColumn(Bytes.toBytes(columnFamily),Bytes.toBytes(columnName)); // delete.addColumns(Bytes.toBytes(columnFamily),Bytes.toBytes(columnName)); // 删除数据 try { table.delete(delete); } catch (IOException e) { e.printStackTrace(); } // 关闭table table.close(); }
删除前,我们的历史版本有3个,最新的是 ls
删除后,name变为了 zs
至此,HBase的基本使用已经完毕,还是比较简单的,解下来就是深入了解一下HBase的底层,毕竟没有学习难度的能力就没有竞争力!