使用Phoenix將SQL代碼移植至HBase

1.前言html

HBase是雲計算環境下最重要的NOSQL數據庫,提供了基於Hadoop的數據存儲、索引、查詢,其最大的優勢就是能夠經過硬件的擴展從而幾乎無限的擴展其存儲和檢索能力。可是HBase與傳統的基於SQL語言的關係數據庫不管從理念仍是使用方式上都相去甚遠,以致於要將基於SQL的項目移植到HBase時每每須要重寫整個項目。java

爲了解決這個問題,不少開源項目提供了HBase的類SQL中間件,意即提供一種在HBase上使用的類SQL語言,使得程序員可以像使用關係數據庫同樣使用HBase,Apache Phoenix就是其中的一個優秀項目。linux

本文介紹瞭如何將基於傳統關係數據庫的程序經過Apache Phoenix移植到基於HBase的雲計算平臺上的方法,並詳細講述了該過程當中碰到的種種困難。主要內容包括:程序員

HBase及雲計算環境的安裝配置;
HBase的Java API編程;
Phoenix的安裝配置與使用;
Squirrel的安裝配置與使用;
使用Phoenix移植SQL代碼至HBase;
Phoenix性能調優;
本文的讀者應該是數據庫系統項目的開發人員和維護人員,雲計算項目開發人員,最好具備如下基本知識:sql

Linux系統使用常識;
Hadoop、Hbase、Zookeeper等雲計算環境使用常識;
Java編程開發基礎;
SQL語言基礎;
Oracle、SQLServer或Mysql等關係數據庫使用管理基礎;shell

2.HBase及雲計算環境的安裝配置數據庫

2.1環境配置apache

雲計算環境一般安裝在linux或者CentOS等類UNIX操做系統中,本文涉及的軟件至少須要三個,即Hadoop、Hbase和Zookeeper,其版本號以下:編程

hadoop-2.3.0-cdh5.1.0
zookeeper-3.4.5-cdh5.1.0
hbase-0.98.1-cdh5.1.0
注意:本文使用了雲時代的版本5.1.0,因爲此類軟件版本衆多,互相之間的兼容性複雜,所以最好統一採用cdh的版本。系統配置以下圖所示:
windows

系統一共六個節點,即Node1~Node6,hadoop安裝在所有六個節點上,其中Node1和Node2是NameNode,其餘是DataNode;ZooKeeper安裝在Node四、Node5和Node6上,其端口使用默認的2181;Hbase安裝在Node一、Node3~Node6上,其中Node1是HMaster,其餘是HRegionServer。具體參數配置能夠參考其餘文檔,此處不作詳細描述。

注意:客戶端必須經過ZooKeeper找到Hbase的入口。對於客戶來講,只須要知道ZooKeeper在哪兒;須要訪問hbase時,客戶端去找ZooKeeper,ZooKeeper再去查詢HBase的HMaster和HRegionServer等信息,具體狀況見《HBase實戰》63頁。

2.2HBase Shell使用

環境配置成功後,便可使用HBase Shell對HBase數據庫進行操做,相似於Oracle提供的sqlplus。

登錄任意一個安裝了HBase的服務器,輸入:

hbase shell
list
便可列出該hbase中存儲的全部表格。
建立一個名爲test的表格,它帶有一個名爲cf的列族,並使用list來查看錶格是否被建立,而後插入一些數據:

hbase(main):003:0> create 'test', 'cf'
0 row(s) in 1.2200 seconds
hbase(main):003:0> list
test
1 row(s) in 0.0550 seconds
hbase(main):004:0> put 'test', 'row1', 'cf:a', 'value1'
0 row(s) in 0.0560 seconds
hbase(main):005:0> put 'test', 'row2', 'cf:b', 'value2'
0 row(s) in 0.0370 seconds
hbase(main):006:0> put 'test', 'row3', 'cf:c', 'value3'
0 row(s) in 0.0450 seconds
使用scan來查看test表格中的內容:

hbase(main):007:0> scan 'test'
ROW COLUMN+CELL
row1 column=cf:a, timestamp=1288380727188, value=value1
row2 column=cf:b, timestamp=1288380738440, value=value2
row3 column=cf:c, timestamp=1288380747365, value=value3
3 row(s) in 0.0590 seconds

獲得表中的一行數據:

hbase(main):008:0> get 'test', 'row1'
COLUMN CELL
cf:a timestamp=1288380727188, value=value1
1 row(s) in 0.0400 seconds

disable和drop一個表格:

hbase(main):012:0> disable 'test'
0 row(s) in 1.0930 seconds
hbase(main):013:0> drop 'test'
0 row(s) in 0.0770 seconds

退出shell:

hbase(main):014:0> exit
其餘更多具體的命令請參看HBase的手冊或者在線幫助。

3.HBase Java API 編程

使用HBase的Java API進行開發須要掌握HBase的基本理念,推薦閱讀《HBase實戰》一書。

在進行開發的操做系統(例如Windows、Linux或者CentOS)中解壓hbase-0.98.1-cdh5.1.0.tar.gz,獲得開發所依賴的全部jar包,位於hbase-0.98.1-cdh5.1.0/lib目錄中。

在開發環境(例如Eclipse、NetBean或者Intellij)中創建工程,導入hbase-0.98.1-cdh5.1.0\lib中的全部jar包。

3.1關於遠程鏈接HBase

在給出源代碼以前,先介紹一下遠程鏈接HBase的問題。從Oracle時代過來的程序員,顯然指望獲得數據庫服務器的ip、port和Service Name之類的信息。可是在鏈接HBase時,你須要的倒是一個或多個ZooKeeper服務器的ip(或者hostname)和port,由於只有它才知曉整個HBase集羣的元數據。

顯然,使用hostname比使用ip要顯得習慣更好,由於它帶來了更大的可移植性,所以費一點筆墨講講linux和windows的hostname設置。

在linux下,hostname經過修改/etc/hosts文件來完成,在集羣的每臺服務器上加入以下內容:

192.168.1.101 Node1
192.168.1.102 Node2
192.168.1.103 Node3
192.168.1.104 Node4
192.168.1.105 Node5
192.168.1.106 Node6
在各自的/etc/sysconfig/network文件中,將「HOSTNAME=」修改成「HOSTNAME=Node?」(將Node?替換爲本服務器的hostname)。

在Windows下(僅測試過Win7 64),修改Windows/System32/drivers/etc/hosts文件,加入:

192.168.1.101 Node1
192.168.1.102 Node2
192.168.1.103 Node3
192.168.1.104 Node4
192.168.1.105 Node5
192.168.1.106 Node6
(不一樣的windows平臺hosts文件的位置可能不同,建議裝一個everything,桌面搜索速度極快)。

其實多種方法均可以鏈接到ZooKeeper,例如ip加端口:

public static String hbase_svr_ip = "192.168.1.104,
192.168.1.105, 192.168.1.106";
public static String hbase_svr_port =
"2181";
或者hostname加端口:

public static String hbase_svr_hostname = "Node4,Node5,Node6";
public static String hbase_svr_port =
"2181";
或者將端口直接寫在ip後:

public static String hbase_svr_ip = "192.168.1.104:2181,
192.168.1.105:2181, 192.168.1.106:2181";

或者將端口直接寫在hostname後:

public static String hbase_svr_hostname = "Node4:2181,Node5:2181,Node6:2181";

或者僅使用一個ZooKeeper服務器:

public static String hbase_svr_hostname = "Node4:2181";

具體使用哪一種方法就看程序員本身的偏好,也存在某種方法在某些版本中可能沒法鏈接的問題,本文中沒有窮盡測試,但我的認爲hostname加端口的方法可能比較穩妥。

3.2源代碼

本篇給出了使用Java API操做HBase的源代碼,注意要將這幾行替換爲實際的ZooKeeper服務器地址、hostname和端口號:

public static String hbase_svr_ip = "192.168.1.104,
192.168.1.105, 192.168.1.106";
public static String hbase_svr_port =
"2181";
public static String hbase_svr_hostname = "Node4,Node5,Node6";
代碼功能包括:

遠程鏈接Hbase數據庫;
建立表;
掃描全部表;
插入數據;
掃描數據;
刪除數據;
刪除表。
package com.wxb;

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

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.Cell;
import org.apache.hadoop.hbase.CellUtil;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HColumnDescriptor;
import org.apache.hadoop.hbase.HTableDescriptor;
import org.apache.hadoop.hbase.client.Delete;
import org.apache.hadoop.hbase.client.Get;
import org.apache.hadoop.hbase.client.HBaseAdmin;
import org.apache.hadoop.hbase.client.HConnection;
import org.apache.hadoop.hbase.client.HConnectionManager;
import org.apache.hadoop.hbase.client.HTableInterface;
import org.apache.hadoop.hbase.client.Put;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.client.Scan;
import org.apache.hadoop.hbase.util.Bytes;

/**

  • @author wxb hbase的基本操做方法
    */
    public class HBaseSample {
    public static String hbase_svr_ip = "192.168.1.104, 192.168.1.105, 192.168.1.106";
    public static String hbase_svr_port = "2181";
    public static String hbase_svr_hostname = "Node4,Node5,Node6";
    private HConnection connection = null;
    Configuration config = null;

    /**
    • 構造函數,構造一個HBaseSample對象,必須在最後調用close方法來關閉全部的鏈接,釋放全部的資源
      */
      public HBaseSample() {
      config = HBaseConfiguration.create();
      config.set("hbase.zookeeper.quorum", hbase_svr_hostname);
      config.set("hbase.zookeeper.property.clientPort", hbase_svr_port);
      // System.out.println(config.get("hbase.zookeeper.quorum"));
      // System.out.println(config.get("hbase.zookeeper.property.clientPort"));

      try {
      connection = HConnectionManager.createConnection(config);
      } catch (IOException e) {
      e.printStackTrace();
      }
      }

    /**
    • 釋放資源
      */
      public void close() {
      try {
      if (null != connection) {
      connection.close();
      }
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
    /**
    • 建立表格
    • @param tableName
    • @param columnFarily
      */
      public void createTable(final String tableName, String columnFarily) {
      if (null != config) {
      System.out.println("begin create table...");
      HBaseAdmin admin = null;
      try {
      admin = new HBaseAdmin(config);
      if (admin.tableExists(tableName)) {
      System.out.println(tableName + " is already exist!");
      } else {
      HTableDescriptor tableDesc = new HTableDescriptor(tableName);
      tableDesc.addFamily(new HColumnDescriptor(columnFarily));
      admin.createTable(tableDesc);
      System.out.println(tableDesc.toString()
      + " has been created.");
      }
      admin.close();
      } catch (IOException e) {
      e.printStackTrace();
      }
      } else {
      System.out.println("hbase could not connected!");
      }
      }
    /**
    • 向指定表格中添加一行數據
    • @param table
    • @param key
    • @param family
    • @param col
    • @param dataIn
    • @return
      */
      public boolean addOneRecord(String table, String key, String family,
      String col, byte[] dataIn) {
      if (null != connection) {
      try {
      HTableInterface tb = connection.getTable(table);
      Put put = new Put(key.getBytes());
      put.add(family.getBytes(), col.getBytes(), dataIn);
      tb.put(put);
      System.out.println("put data key = " + key);
      return true;
      } catch (IOException e) {
      System.out.println("put data failed.");
      return false;
      }
      } else {
      System.out.println("hbase could not connected!");
      return false;
      }
      }
    /**
    • 獲得hbase中全部的表
    • @return
      */
      public List getAllTables() {
      List tables = null;
      if (connection != null) {
      try {
      HTableDescriptor[] allTable = connection.listTables();
      if (allTable.length > 0)
      tables = new ArrayList ();
      for (HTableDescriptor hTableDescriptor : allTable) {
      tables.add(hTableDescriptor.getNameAsString());
      System.out.println(hTableDescriptor.getNameAsString());
      }
      } catch (IOException e) {
      e.printStackTrace();
      }
      } else {
      System.out.println("hbase could not connected!");
      }
      return tables;
      }

    public byte[] getValueWithKey(String tableName, String rowKey,
    String family, String qualifier) {
    byte[] rel = null;
    if (null != connection) {
    try {
    HTableInterface table = connection.getTable(tableName);
    Get get = new Get(rowKey.getBytes());
    get.addColumn(Bytes.toBytes(family), Bytes.toBytes(qualifier));
    Result result = table.get(get);
    if (!result.isEmpty()) {
    rel = result.getValue(Bytes.toBytes(family),
    Bytes.toBytes(qualifier));
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
    } else {
    System.out.println("hbase could not connected!");
    }
    return rel;
    }

    /**
    • 從表中刪除一行
    • @param tableName
    • @param rowKey
      */
      public void deleteWithKey(String tableName, String rowKey) {
      if (null != connection) {
      try {
      HTableInterface table = connection.getTable(tableName);
      Delete delete = new Delete(rowKey.getBytes());
      table.delete(delete);
      } catch (IOException e) {
      e.printStackTrace();
      }
      } else {
      System.out.println("hbase could not connected!");
      }
      }
    /**
    • 獲得一個表中的全部元素
    • @param tableName
      */
      public void getAllData(String tableName) {
      if (null != connection) {
      try {
      HTableInterface table = connection.getTable(tableName);
      Scan scan = new Scan();
      ResultScanner rs = table.getScanner(scan);
      for (Result r : rs) {
      Cell[] cells = r.rawCells();
      System.out.println("This row have " + cells.length
      + " cells:");
      for (Cell cell : cells) {
      String row = Bytes.toString(CellUtil.cloneRow(cell));
      String family = Bytes.toString(CellUtil
      .cloneFamily(cell));
      String qualifier = Bytes.toString(CellUtil
      .cloneQualifier(cell));
      String value = Bytes
      .toString(CellUtil.cloneValue(cell));
      System.out.println(String.format("%s:%s:%s:%s", row,
      family, qualifier, value));
      }
      }
      } catch (IOException e) {
      e.printStackTrace();
      }
      } else {
      System.out.println("hbase could not connected!");
      }
      }

    public void deleteTable(String tableName) {
    if (null != config) {
    System.out.println("begin delete table...");
    HBaseAdmin admin = null;
    try {
    admin = new HBaseAdmin(config);
    if (!admin.tableExists(tableName)) {
    System.out.println(tableName + " is not exist!");
    } else {
    admin.disableTable(tableName);
    admin.deleteTable(tableName);
    System.out.println(tableName + " has been deleted.");
    }
    admin.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    } else {
    System.out.println("hbase could not connected!");
    }
    }

    /**
    • @param args
      */
      public static void main(String[] args) {
      HBaseSample sample = new HBaseSample();
      // 1.create table and insert data
      sample.createTable("student", "fam1");
      sample.addOneRecord("student", "id1", "fam1", "name", "Jack".getBytes());
      sample.addOneRecord("student", "id1", "fam1", "address",
      "HZ".getBytes());

      // 2.list table
      sample.getAllTables();

      // 3.getValue
      byte[] value = sample.getValueWithKey("student", "id1", "fam1",
      "address");
      System.out.println("value = " + Bytes.toString(value));

      // 4.addOneRecord and delete
      // sample.addOneRecord("student", "id2", "fam1", "name", "wxb".getBytes());
      // sample.addOneRecord("student", "id2", "fam1", "address",
      // "here".getBytes());
      // sample.deleteWithKey("student", "id2");

      // 5.scan table
      sample.getAllData("student");

      // 6.delete table
      // sample.deleteTable("student");

      sample.close();
      }
      }
  1. Phoenix的安裝配置與使用

從上一章能夠看出,HBase的基本理念和傳統的關係數據庫是大相徑庭的,爲了使得熟悉SQL的程序員可以快速使用HBase,使用Apache Phoenix是比較好的辦法。它提供了一組相似於SQL的語法,以及序列、索引、函數等工具,使得將SQL代碼移植至HBase成爲可能。

4.1 Phoenix安裝

同其餘分佈式軟件同樣,Phoenix的安裝也是較爲複雜的,且要密切關注其版本兼容性,不然極可能沒法正常運行。例如Phoenix4.x版本都有兼容HBase0.98的版本,可是通過兩天的測試才發現不一樣的Phoenix版本對HBase0.98的小版本號的要求不一樣。
因爲本文使用的是HBase0.98.1,所以只能使用Phoenix4.1.0版本。若是使用的Phoenix版本和HBase版本不兼容,會出現第一次可以鏈接HBase,但之後都鏈接失敗的現象。
Phoenix的具體安裝步驟以下:
第一步:將phoenix-4.1.0-bin.tar.gz拷貝到Node1(HBase的HMaster)的某路徑下,解壓縮,拷貝hadoop2/phoenix-4.1.0-server-hadoop2.jar到HBase的lib目錄下。
第二步:而後用scp(關於scp和ssh的設置請參考網上的其餘文章,假設用戶名爲hadoop)拷貝到各個regionserver的HBase的lib目錄下:

scp phoenix-4.1.0-server-hadoop2.jar hadoop@Node3:/home/hadoop/hbase-0.98.1-cdh5.1.0/lib/
phoenix-core-4.6.0-HBase-0.98.jar
scp phoenix-4.1.0-server-hadoop2.jar hadoop@Node4:/home/hadoop/hbase-0.98.1-cdh5.1.0/lib/
phoenix-core-4.6.0-HBase-0.98.jar
scp phoenix-4.1.0-server-hadoop2.jar hadoop@Node5:/home/hadoop/hbase-0.98.1-cdh5.1.0/lib/
phoenix-core-4.6.0-HBase-0.98.jar
scp phoenix-4.1.0-server-hadoop2.jar hadoop@Node6:/home/hadoop/hbase-0.98.1-cdh5.1.0/lib/
phoenix-core-4.6.0-HBase-0.98.jar
第三步:在HMaster上重啓hbase(即Node1);
第四步:將phoenix-4.1.0-client-hadoop2.jar加入客戶端的CLASSPATH變量路徑中,修改用戶的.bash_profile文件,同時將此文件拷貝到hbase的lib目錄下。
第五步:測試使用phoenix,輸入命令:

sqlline.py Node4:2181
注意:後面的參數是ZooKeeper的服務器和端口。
出現如下顯示則說明鏈接成功。

[hadoop@iips25 hadoop2]$bin/sqlline.py Node1:2181
Setting property: [isolation, TRANSACTION_READ_COMMITTED]
issuing: !connect jdbc:phoenix:Node4 none none org.apache.phoenix.jdbc.PhoenixDriver
Connecting to jdbc:phoenix:Node4
16/06/21 08:04:24 WARN impl.MetricsConfig: Cannot locate configuration: tried hadoop-metrics2-phoenix.properties,hadoop-metrics2.properties
Connected to: Phoenix (version 4.1)
Driver: org.apache.phoenix.jdbc.PhoenixDriver (version 4.1)
Autocommit status: true
Transaction isolation: TRANSACTION_READ_COMMITTED
Building list of tables and columns for tab-completion (set fastconnect to true to skip)...
59/59 (100%) Done
Done
sqlline version 1.1.2
0: jdbc:phoenix:Node4>
查看數據庫表:(注意,phoenix只能看到本身建立的表,不能看到HBase建立的表)

0: jdbc:phoenix:Node4> !tables
+------------+-------------+------------+------------+------------+------------+---------------------------+----------------+-------------+----------------+--------+
| TABLE_CAT | TABLE_SCHEM | TABLE_NAME | TABLE_TYPE | REMARKS | TYPE_NAME | SELF_REFERENCING_COL_NAME | REF_GENERATION | INDEX_STATE | IMMUTABLE_ROWS | SALT_B |
+------------+-------------+------------+------------+------------+------------+---------------------------+----------------+-------------+----------------+--------+
| null | SYSTEM | CATALOG | SYSTEM TABLE | null | null | null | null | null | false | null |
| null | SYSTEM | SEQUENCE | SYSTEM TABLE | null | null | null | null | null | false | null |
+------------+-------------+------------+------------+------------+------------+---------------------------+----------------+-------------+----------------+--------+
0: jdbc:phoenix:Node4>
建立表,並插入數據:

0: jdbc:phoenix:Node4> create table abc(a integer primary key, b integer) ;
No rows affected (1.133 seconds)
0: jdbc:phoenix:Node4> UPSERT INTO abc VALUES (1, 1);
1 row affected (0.064 seconds)
0: jdbc:phoenix:Node4> UPSERT INTO abc VALUES (2, 2);
1 row affected (0.009 seconds)
0: jdbc:phoenix:Node4> UPSERT INTO abc VALUES (3, 12);
1 row affected (0.009 seconds)
0: jdbc:phoenix:Node4> select * from abc;
+------------+------------+
| A | B |
+------------+------------+
| 1 | 1 |
| 2 | 2 |
| 3 | 12 |
+------------+------------+
3 rows selected (0.082 seconds)
0: jdbc:phoenix:Node4>
建立包含中文的表(注意中文要使用VARCHAR):

create table user ( id integer primary key, name VARCHAR);
upsert into user values ( 2, '測試員2');
upsert into user values ( 1, '測試員1');
select * from user;
+------------+------------+
| ID | NAME |
+------------+------------+
| 1 | 測試員1 |
| 2 | 測試員2 |
4.2 phoenix配置

在hbase集羣每一個服務器的hbase-site.xml配置文件中,加入:


hbase.regionserver.wal.codec
org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec

這是在phoenix中創建索引的先決條件。若是不添加此設置,Phoenix依然能夠正常使用,但不能創建索引。

4.3 phoenix語法簡介

phoenix的語法可參考其官方網站,也可下載其「Grammar _ Apache Phoenix.html」網頁。
訪問Phoenix時,可使用其提供的sqlline.py命令,也可使用下一章介紹的數據庫圖形界面工具Squirrel,固然也能夠經過Phoenix提供的Java API。

4.3.1. 建立表

注意:Phoenix中的表必須有主鍵,這一點和許多關係數據庫不一樣。由於主鍵是後續不少表操做的必備因素。

CREATE TABLE IF NOT EXISTS MYTABLE (ID INTEGER PRIMARY KEY, NAME VARCHAR, SEX VARCHAR, ADDRESS VARCHAR);
4.3.2. 刪除表

DROP TABLE IF EXISTS MYTABLE;
4.3.3. 插入數據

UPSERT INTO MYTABLE VALUES (1, 'WXB', 'MALE', '010-22222222');
注意phoenix使用UPSERT而不是INSERT。

4.3.4. 刪除數據

DELETE FROM MYTABLE WHERE ID = 1;
4.3.5. 查詢數據

SELECT * FROM MYTABLE WHERE ID=1;
4.3.6. 修改數據

UPSERT INTO MYTABLE VALUES (1, 'WXB', 'MALE', '010-22222222');
能夠看到,修改數據與插入數據同樣,都是使用UPSERT語句,若此主鍵對應的行不存在,就插入,不然就修改。這也是爲何Phoenix的表必須有主鍵的緣由之一。

4.3.7. 建立序列

Phoenix的序列與Oracle很像,也是先建立,而後調用next獲得下一個值。也能夠繼續調用current value獲得當前序列值,沒有調用next時,不能使用current value。
建立一個序列:

CREATE SEQUENCE IF NOT EXISTS WXB_SEQ START WITH 1000 INCREMENT BY 1 MINVALUE 1000 MAXVALUE 999999999 CYCLE CACHE 30;
其含義基本上與Oracle相似。

4.3.8. 使用序列

序列只能在Select或者Upsert語句中使用,例如在Upsert中使用:

UPSERT INTO MYTABLE VALUES (NEXT VALUE FOR WXB_SEQ, 'WXB', 'MALE', '010-22222222');
讀取序列的當前值時,採用這個語句:

SELECT CURRENT VALUE FOR WXB_SEQ DUALID FROM WXB_DUAL;
而後讀取DUALID就可獲得序列的當前值。
這裏的WXB_DUAL是我本身建立的一個特殊表,用來模擬Oracle中的Dual表。

CREATE TABLE IF NOT EXISTS WXB_DUAL (DUALID INTEGER PRIMARY KEY );
UPSERT INTO WXB_DUAL VALUES (1);
4.3.9. 刪除序列

DROP SEQUENCE IF EXISTS WXB_SEQ;

本章至此爲止,詳細的操做留待後續再講。

  1. 安裝SQuirrel

Squirrel是一個圖形化的數據庫工具,它能夠將Phoenix以圖形化的方式展現出來,它能夠安裝在windows或linux系統中。

5.1 安裝步驟

第一步:
設置好JDK,JAVA_HOME,CLASSPATH等一系列的環境變量,注意不管是在windows仍是在linux下,都須要上面安裝的hbase和phoenix的存放jar包的目錄,並將其設置到CLASSPATH中。windows下的CLASSPATH以下:

%JAVA_HOME%\lib;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;D:\hbase-0.98.1-cdh5.1.0\lib;D:\phoenix-4.1.0-bin\hadoop2
linux的CLASSPATH以下:

export PHOENIX_HOME=/home/hadoop/phoenix-4.1.0-bin
export CLASSPATH=$PHOENIX_HOME/hadoop2/phoenix-4.1.0-client-hadoop2.jar:$HBASE_HOME/lib/:$CLASSPATH
export PATH=$PHOENIX_HOME/bin:$PATH

第二步:
下載解壓squirrel-sql-snapshot-20160613_2107-standard.jar(最新版本的squirrel安裝包),在命令行中運行java -jar squirrel-sql-snapshot-20160613_2107-standard.jar開始安裝。
第三步:執行以下安裝

  1. Remove prior phoenix-[oldversion]-client.jar from the lib directory of SQuirrel, copy phoenix-[newversion]-client.jar to the lib directory (newversion should be compatible with the version of the phoenix server jar used with your HBase installation)
  2. Start SQuirrel and add new driver to SQuirrel (Drivers -> New Driver)
  3. In Add Driver dialog box, set Name to Phoenix, and set the Example URL to jdbc:phoenix:localhost.
  4. Type 「org.apache.phoenix.jdbc.PhoenixDriver」 into the Class Name textbox and click OK to close this dialog.
  5. Switch to Alias tab and create the new Alias (Aliases -> New Aliases)
  6. In the dialog box, Name:Any name, Driver: Phoenix, User Name:Anything, Password:Anything
  7. Construct URL as follows: jdbc:phoenix:zookeeper quorum server. For example, to connect to a local HBase use: jdbc:phoenix:localhost
  8. Press Test (which should succeed if everything is setup correctly) and press OK to close.
  9. Now double click on your newly created Phoenix alias and click Connect. Now you are ready to run SQL queries against Phoenix.
    注意,咱們鏈接的URL是jdbc:phoenix:Node4,用戶名和密碼隨意便可。鏈接成功後,以下:

5.2 使用

安裝完畢後,就能夠在Squirrel中執行各類phoenix支持的類SQL語句和觀察數據了,例如在SQL欄中輸入以下語句:

CREATE TABLE IF NOT EXISTS MYTABLE (ID INTEGER PRIMARY KEY, NAME VARCHAR, SEX VARCHAR, ADDRESS VARCHAR);

UPSERT INTO MYTABLE VALUES (1, 'WXB', 'MALE', '010-22222222');

UPSERT INTO MYTABLE VALUES (2, ‘LL’, 'MALE', '010-11111111');

SELECT * FROM MYTABLE;

結果以下:

使用Squirrel的好處在於能夠方便的查看數據庫中的各類對象,以及編輯和執行復雜的phoenix類sql腳本。

  1. 使用Phoenix移植SQL代碼至HBase

Phoenix提供了徹底適配JDBC的API,程序員能夠像操做關係數據庫(例如Oracle)同樣來使用JDBC來操做Phoenix,這也是Phoenix的最大的優點所在。惟一須要注意的是,提交的SQL語句必須符合Phoenix語法,雖然此語法很相似於SQL,但仍是有許多不一樣之處。

6.1 Phoenix Java Coding

本章給出了一個最基本的Phoenix JDBC源代碼實例,注意其中所引用的全部類幾乎都來自於java.sql.*包,與Oracle惟一的不一樣是其driver的字符串,該字符串等於前面鏈接Squirrel的鏈接字符串,你能夠在Squirrel上測試driver字符串是否可以正確鏈接。driver字符串通常爲jdbc:phoenix:ZooKeeper_hostname:port,例如jdbc:phoenix:Node4,Node5,Node6:2181。可是在端口爲默認2181端口時,也能夠省略端口號。
編碼以前將phoenix-4.1.0-client-hadoop2.jar加入java項目的依賴Libraries,例子代碼以下:

package com.wxb;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;

/**

  • @author wxb Phoenix的基本操做方法
  • */
    public class PhoenixSample {
    public static String hbase_svr_ip = "192.168.1.104, 192.168.1.105, 192.168.1.106";
    public static String hbase_svr_port = "2181";
    public static String hbase_svr_hostname = "Node4,Node5,Node6";

    /*
    • 全部幾種方式的driver都可以經過測試: 1.Node4 2.Node4,Node5,Node6 3.Node4:2181
    • 4.Node4,Node5,Node6:2181 5.Node4:2181,Node5:2181,Node6:2181
    • 6.101.60.27.114
      */
      public static String driver = "jdbc:phoenix:" + hbase_svr_hostname;

    public static void createTable(String tableName) {
    System.out.println("create table " + tableName);
    Statement stmt = null;

    try {
         Connection con = DriverManager.getConnection(driver);
         stmt = con.createStatement();
    
         stmt.executeUpdate("create table  if not exists " + tableName
                 + " (mykey integer not null primary key, mycolumn varchar)");
         con.commit();
         con.close();
     } catch (SQLException e) {
         e.printStackTrace();
     }

    }

    public static void addRecord(String tableName, String values) {
    Statement stmt = null;

    try {
         Connection con = DriverManager.getConnection(driver);
         stmt = con.createStatement();
    
         stmt.executeUpdate("upsert into " + tableName + " values ("
                 + values + ")");
         con.commit();
         con.close();
     } catch (SQLException e) {
         e.printStackTrace();
     }

    }

    public static void deleteRecord(String tableName, String whereClause) {
    Statement stmt = null;

    try {
         Connection con = DriverManager.getConnection(driver);
         stmt = con.createStatement();
    
         stmt.executeUpdate("delete from " + tableName + " where "
                 + whereClause);
         con.commit();
         con.close();
     } catch (SQLException e) {
         e.printStackTrace();
     }

    }

    public static void createSequence(String seqName) {
    System.out.println("Create Sequence :" + seqName);
    Statement stmt = null;

    try {
         Connection con = DriverManager.getConnection(driver);
         stmt = con.createStatement();
    
         stmt.executeUpdate("CREATE SEQUENCE IF NOT EXISTS "
                 + seqName
                 + " START WITH 1000 INCREMENT BY 1 MINVALUE 1000 MAXVALUE 999999999 CYCLE CACHE 30");
         con.commit();
         con.close();
     } catch (SQLException e) {
         e.printStackTrace();
     }

    }

    public static void dropSequence(String seqName) {
    System.out.println("drop Sequence :" + seqName);
    Statement stmt = null;

    try {
         Connection con = DriverManager.getConnection(driver);
         stmt = con.createStatement();
    
         stmt.executeUpdate("DROP SEQUENCE IF EXISTS " + seqName);
         con.commit();
         con.close();
     } catch (SQLException e) {
         e.printStackTrace();
     }

    }

    public static void getAllData(String tableName) {

    System.out.println("Get all data from :" + tableName);
     ResultSet rset = null;
    
     try {
         Connection con = DriverManager.getConnection(driver);
         PreparedStatement statement = con.prepareStatement("select * from "
                 + tableName);
         rset = statement.executeQuery();
         while (rset.next()) {
             System.out.print(rset.getInt("mykey"));
             System.out.println(" " + rset.getString("mycolumn"));
         }
         statement.close();
         con.close();
     } catch (SQLException e) {
         e.printStackTrace();
     }

    }

    public static void dropTable(String tableName) {

    Statement stmt = null;
    
     try {
         Connection con = DriverManager.getConnection(driver);
         stmt = con.createStatement();
    
         stmt.executeUpdate("drop table  if  exists " + tableName);
         con.commit();
         con.close();
         System.out.println("drop table " + tableName);
     } catch (SQLException e) {
         e.printStackTrace();
     }

    }

    public static void main(String[] args) {
    createTable("wxb_test");
    createSequence("WXB_SEQ_ID");

    // 使用了Sequence
     addRecord("wxb_test", "NEXT VALUE FOR WXB_SEQ_ID,'wxb'");
     addRecord("wxb_test", "NEXT VALUE FOR WXB_SEQ_ID,'wjw'");
     addRecord("wxb_test", "NEXT VALUE FOR WXB_SEQ_ID,'wjl'");
    
     // deleteRecord("wxb_test", " mykey = 1 ");
     getAllData("wxb_test");
    
     // dropTable("wxb_test");

    // dropSequence("WXB_SEQ_ID");

    }
    }
    6.2 每一個表必須包含一個主鍵

在使用Phoenix時,創建的每一個表都必須包含一個主鍵,這與關係數據庫不一樣。並且每一個表的主鍵會自動被索引,這意味着在select語句的where子句中使用主鍵做爲條件,會獲得最快的查詢速度。關於索引,在後續章節中再詳細介紹。
個人建議是,爲每一個表建立一個序列,並在插入數據時以序列的值做爲主鍵的值。

6.3 JDBC鏈接池

Phoenix支持用戶本身建立JDBC鏈接池,能夠將基於JDBC鏈接池的代碼複製過來,把Driver部分修改一番便可。

6.4 中文支持

涉及中文的字段可設置爲VARCHAR類型,經測試沒有問題。

6.5 CLOB和BLOB

CLOB和BLOB字段我都設置爲VARCHAR類型,經測試存儲400k字節的數據沒有問題,更多的沒有測試。

6.6 複雜的SQL語句

由於本文使用的Phoenix版本不是最新版,所以官網上給出的SQL語法不是徹底都可以支持,例以下面的語句就不能支持:

delete from wxb_senword where swid in (select swid from wxb_rela_sw_group where groupid=1)
所以對於一些複雜的SQL語句,須要先到官網上查詢語法,而後在phoenix中進行測試,測試經過後纔可以在程序中使用。
兩個表的關聯查詢是可行的,語句以下:

SELECT d.swid,d.swname, d.userid, e.groupid FROM wxb_senword d JOIN wxb_rela_sw_group e ON e.swid = d.swid where e.groupid=1;

  1. Phoenix性能調優

7.1 代碼移植流程

將基於SQL的java代碼移植到Phoenix其實不難,以Oracle爲例,基本流程以下:

將Oracle中的全部表在Phoenix中從新創建一次,沒有主鍵的本身加一個主鍵(並創建對應的序列);
將Oracle中全部的序列、視圖都在Phoenix中從新創建一次;
將程序中的每條SQL語句都翻譯爲Phoenix的SQL語句,並測試該語句是否可以正確運行,若不能,總能找到幾條簡單的語句進行替代。
7.2 Oracle和HBase的性能差別

移植完成後,通過一系列debug,程序總算可以正常運行了。可是性能問題會變得很是嚴重,這是關係數據庫和HBase之間的設計思路和應用問題域之間的差別形成的。
Oracle的設計思路是儘量的快速對數據進行操做,可是隨着表中記錄數的不斷增長,查詢性能持續降低。要對Oracle進行硬件擴充會比較困難,並且會在單表一億條左右時(沒有通過本人驗證)碰到性能瓶頸。Oracle的優點是在表中記錄數很少(幾百萬之內,具體看服務器性能)時擁有極高的查詢速度。
而HBase的優點是讓單表能夠存儲幾乎無限的記錄,而且能夠方便的擴充硬件,使得查詢速度能夠達到一個穩定的標準。可是其缺點在於表中數據很少時,查詢速度相對較慢。經測試,Phoenix的表在記錄數不多時(數十條),查詢單條數據也須要0.2秒左右(服務器集羣配置見前面的章節),而同時單服務器的Oracle查詢這樣的數據僅需30ms左右,相差接近十倍。

7.3 Phoenix索引性能測試

與Oracle相比,Phoenix在性能上還有一個特色就是在沒有索引的狀況下,查詢性能降低很快。
例以下表:

CREATE TABLE IF NOT EXISTS WXB_WORD (ID INTEGER PRIMARY KEY, NAME VARCHAR, VALUE DOUBLE, HEAT INTEGER, FOCUSLEVEL INTEGER, USERID INTEGER);
不創建索引的狀況下,在前面介紹的集羣上進行查詢性能測試,查詢語句以下(確保單條命中):

SELECT * FROM WXB_WORD WHERE NAME=’XXX’;
50萬條記錄,平均單條查詢時間爲0.38秒;
100萬條記錄,平均單條查詢時間爲0.79秒;
500萬條記錄,平均單條查詢時間爲4.31秒;
然而在NAME字段上創建索引後,將表中數據增長到1億條,平均單條查詢時間爲0.164秒,可見索引對Phoenix性能的提高做用是無可替代的。

7.4 Phoenix索引簡介

Phoenix中的索引被稱之爲Secondary Indexing(二級索引),這是爲了和HBase主鍵上的索引區分開。在HBase中,每一個表有且僅有一個主鍵的索引,該索引按照字典序進行排序;全部不基於主鍵的查詢都會致使全表掃描,效率很是低下。在Phoenix中,能夠對錶中的任何一個字段或者幾個字段創建二級索引,該索引其實是一個獨立的表,表中包含了被索引的列以及創建索引時包含的列(在索引的include語句中包含的列)。當用戶對錶進行查詢時,會首先對索引進行查詢,若可以獲得所有的結果,則會直接返回,不然就到原表中進行查詢。
注意,Phoenix的每一個表均可以創建多個索引,索引和原表之間的同步由Phoenix保證。可是,索引越多,寫入效率越低。
Phoenix支持兩種類型的索引:可變索引(mutable indexing)和不可變索引(immutable indexing)。在表中數據須要變化時,使用可變索引;當應用場景爲「一次寫入,只會追加,永不改變」時使用不可變索引。本文中只使用了可變索引。

7.5 創建索引的方法與語句

在創建索引以前,再次檢查Phoenix的配置,在HBase集羣的每一個服務器的hbase-site.xml配置文件中,加入:


hbase.regionserver.wal.codec
org.apache.hadoop.hbase.regionserver.wal.IndexedWALEditCodec

例如:在WXB_WORD表上對NAME字段創建DESC索引,該索引還包含了VALUE字段的值(注意,Phoenix是大小寫不敏感的)。

create index if not exists idx_wxb_word on wxb_word (name desc) include (value) ;
那麼這種語句就查詢得特別快:

select name,value from wxb_word where name='AHNHLYPKGYAR_59999';
可是若是查詢語句中還須要知道其餘字段的值,例如:

select name,value,userid from wxb_word where name='AHNHLYPKGYAR_59999';
那麼,就和沒有索引差很少,由於該索引中沒有包含userid這個字段。
另外須要注意的是:主鍵不須要索引,查詢也很是快,這是由HBASE的特性保證的。
刪除索引語句:

drop index if exists idx_wxb_word on wxb_word;

  1. 總結

使用Phoenix將SQL代碼移植到HBase應注意如下幾個問題。

第一,應用場景是否合適?是否須要在單表中存儲幾乎無限的數據,並保證必定的查詢性能?在數據量較少的情景下,Phoenix反而比Oracle的性能差。若要追求最高的性能,能夠考慮同時使用關係數據庫和HBase,並本身保證這部分數據的同步。 第二,Phoenix、HBase、Hadoop、ZooKeeper的版本兼容問題。在大部分狀況下,開發人員並不能決定HBase、Hadoop和ZooKeeper的版本,所以只能尋找合適的Phoenix版原本適配它們,這將致使你不能使用最新的Phoenix版本。如同本文中寫的同樣,這種狀況會致使一些Phoenix SQL語句的特性得不到支持。 第三,注意Phoenix的每一個表必須包含一個主鍵(其實就是HBase的Primary rowkey),且該主鍵自帶索引,合理設計這個主鍵可以帶來性能上的提高和查詢的便利。做爲從SQL時代過來的程序員,拋棄節約空間的想法;在大數據時代,就是儘量的用空間換時間。舉個例子,你甚至能夠將全部字段以必定的順序和分隔符所有堆到主鍵上。 第四,移植代碼時,將全部SQL語句一一翻譯爲對應的Phoenix語句便可。注意參考Phoenix主頁上的語法介紹,並一一進行測試。Phoenix對JDBC的支持很好,諸如鏈接池一類的特性能夠原封不動的照搬。但若原來的程序使用了針對SQL語句的中間件之類的技術,請恕我也不知如何處理。 第五,必定要對Phoenix的表創建二級索引,索引中儘量包含全部須要查詢的字段。索引會致使數據插入速度變慢,但會帶來巨大的性能提高。

相關文章
相關標籤/搜索