經過多次修改運行非常穩定,已經用於生產環境。內部線程通訊使用wait()/notify()機制,效率很高。本文只在Java層面討論HBase的多線程,HBase API內部多線程機制不在本文討論範圍之內。HBase用戶端需要的jar包自行下載,這裡就不提供了,本類源碼結尾處可以下載。下面通過demo示範一下用法,結尾處有代碼打包下載。
特點:
■從Java層面用多線程最大化了讀寫效能。
■大量減少了應用程式的代碼量,現在可以集中精力到資料分析上了。
■效能平均下來至少比單線程高出幾個數量級。
HScan
HScan有5個public方法:
1.public HScan(String table, String[] columns, int threadNum),
table指定表名,columns指定掃描的列,如果設為null代表掃描所有的列,如果設為空白數組代表只掃描rowkey,threadNum設定掃描器的並發數。
2.public void addTask(String start, String end, int num),前兩個參數是起止rowkey,第三個參數用於控制rowkey平分成num段,這個參數主用用於可整型計算的時間戳記開頭的rowkey,如果非整型範圍內數字這個參數設為1即可。這個方法可以調用多次,每次添加num個任務到任務池。
3.public void fuck(),繼承來的方法,開始進行掃描。
4.public HashMap<String, String> fetch(),從緩衝區裡取一個結果出來,結果中的鍵由列名和”rowKey”組成,如果返回null表示所有行都已經讀完了。
5.public void status(),一個非同步方法,用於近似顯示線程的運行狀態,總是在當前行顯示,內容由4部分組成,第一列顯示已經返回的結果數,第二列顯示緩衝區中的行數,第三列分別表示 當前並發線程數/Thread.State.WAITING數/Thread.State.BLOCKED數,第四列顯示任務池中的任務數。
Demo,掃描gm_player_detail表2013年1月1日到10日的所有資料
| 代碼如下 |
複製代碼 |
package joyport.hbase.gm;
import java.util.HashMap; public class Test { private static int threadNum = 30; private static int taskNum = 1000; private static String htable = "gm_player_detail"; public static void main(String[] args) throws Exception { int[] time = new int[2]; if (args.length == 2) { time = Util.getTimeScale(args[0], args[1]); } else if (args.length == 1) { time = Util.getTimeScale(-1); time[0] = Util.getTimeScale(args[0], args[0])[0]; } else { time = Util.getTimeScale(-1); } HScan hScan = new HScan(htable, null, threadNum); hScan.addTask(String.valueOf(time[0]), String.valueOf(time[1]), taskNum); hScan.fuck(); Test test = new Test(); test.analyse(hScan); } public void analyse(HScan hScan) throws InterruptedException { HashMap<String, String> row = null; for (row = hScan.fetch(); row != null; row = hScan.fetch()) { hScan.status(); //System.out.println(row); } } } |
Util.getTimeScale(-1)擷取昨天的起止時間戳記
Util.getTimeScale(args[0], args[1])根據yyyy:mm:dd格式的日期擷取起止時間戳記,java用的是微秒計算的時候可能跨天,如果總是用這個函數就不會出現時間斷裂或重複。
編譯然後運行
java joyport.hbase/gm/Test 2013-01-01 2013-01-10
HGet
get操作還是很有用途的,比如有些需求需要這樣來做,統計每一天的前n條記錄,但是一條記錄欄位特別多,可以先只Scan必要欄位計算出前n,然後根據rowkey再回去get詳細資料,比直接在詳細資料上計算效能高多了。因為需要get的rowkey都是放在記憶體的,所以如果需要大量get需要自己控制記憶體使用量率。public 方法和HScan類似,自己看代碼就明白。
Demo,只掃描rowkey然後根據rowkey用get擷取所有列。
| 代碼如下 |
複製代碼 |
package joyport.hbase.gm;
import java.util.HashMap; public class Test { private static int threadNum = 30; private static int taskNum = 1000; private static String htable = "gm_player_detail"; public static void main(String[] args) throws Exception { int[] time = new int[2]; if (args.length == 2) { time = Util.getTimeScale(args[0], args[1]); } else if (args.length == 1) { time = Util.getTimeScale(-1); time[0] = Util.getTimeScale(args[0], args[0])[0]; } else { time = Util.getTimeScale(-1); } String[] cols = {}; HScan hScan = new HScan(htable, cols, threadNum); hScan.addTask(String.valueOf(time[0]), String.valueOf(time[1]), taskNum); hScan.fuck(); Test test = new Test(); test.analyse(hScan); } public void analyse(HScan hScan) throws Exception { HashMap<String, String> row = null; HGet hGet = new HGet(htable, null, 30); for (row = hScan.fetch(); row != null; row = hScan.fetch()) { hScan.status(); //System.out.println(row); hGet.addTask(row.get("rowKey")); } hGet.fuck(); for (row = hGet.fetch(); row != null; row = hGet.fetch()) { hScan.status(); //System.out.println(row); } } }
|
HPut
HPut和HScan,HGet不太一樣,因為put操作的資料來源可以有多個,而且類型可以任意(從文本,資料庫,HBase表,流),所以對資料來源線程也進行了封裝,資料來源線程數由使用者決定。
1.public HPut(String table, int threadNum) threadNum是寫入線程的數目。
2.public void addTask(final Callable<HashMap<String, String>> task) 返回一行記錄的介面,每調用一次產生一個新的資料來源線程,當返回null的時候資料來源線程結束。
3.public void enableStatus(boolean enable),即使是一個近似的狀態也需要在合適的地方顯示,使用者程式中顯示基本上誤差非常大,所以放到put操作的時候顯示,這個函數用來控制是否顯示put狀態,預設顯示。
Demo,只展示一個資料來源的情況,多資料來源沒有測試(理論上應該沒問題)。從gm_player_detail讀取一天的資料插入到hbase_test表。
| 代碼如下 |
複製代碼 |
package joyport.hbase.gm; import java.util.HashMap; import java.util.concurrent.Callable; public class Test extends Thread implements Callable<HashMap<String, String>> { private int threadNum = 30; private int taskNum = 1000; private String htable = "gm_player_detail"; private HScan hScan; public static void main(String[] args) throws Exception { int[] time = new int[2]; if (args.length == 2) { time = Util.getTimeScale(args[0], args[1]); } else if (args.length == 1) { time = Util.getTimeScale(-1); time[0] = Util.getTimeScale(args[0], args[0])[0]; } else { time = Util.getTimeScale(-1); } Test t1 = new Test(time[0], time[1]); // 往hbase_test寫資料 HPut hPut = new HPut("hbase_test", 10); hPut.addTask(t1); hPut.fuck(); } public Test(int startkey, int endkey) throws Exception { String[] cols = null; hScan = new HScan(htable, cols, threadNum); hScan.addTask(String.valueOf(startkey), String.valueOf(endkey), taskNum); hScan.fuck(); } public HashMap<String, String> call() throws InterruptedException { return hScan.fetch(); } } |
HDelete
delete操作只需要rowkey就可以,但是緩衝區資料結構是死的,所以也使用HashMap<String,String>格式,以rowKey為鍵。如果row.isEmpty()為真則這條資料跳過。
Demo,從hbase_test讀取所有行,並刪除所有行。
| 代碼如下 |
複製代碼 |
package joyport.hbase.gm;
import java.util.HashMap; import java.util.concurrent.Callable; public class Test implements Callable<HashMap<String, String>> { private static int threadNum = 30; private static String htable = "hbase_test"; private static HScan hScan; public static void main(String[] args) throws Exception { String[] cols = {}; hScan = new HScan(htable, cols, threadNum); hScan.addTask("0", "2", 1); hScan.fuck(); HDelete h = new HDelete(htable, 10); h.addTask(new Test()); h.fuck(); } public HashMap<String, String> call() throws Exception { return hScan.fetch(); } } |
需要注意的問題:
■預設緩衝區大小是10000,外部程式線程數(不是構造方法中定義的那個並發)不能超過這個值,否則可能導致無限wait()。
■目前HScan的任務池沒有進行排重處理,也就是兩個任務的起止rowkey交叉和相同任務重複沒有進行內部處理,為避免重複掃描需要外部保證。HGet任務排重也需要外部保證。
■HScan和HGet任務必須一次性添加完畢,因為threadStart()一旦判斷完成將關閉緩衝區而造成任務丟失。
■本類只在Java層面進行多線程操作!HBaseClient內部同步機制不在本類管轄範圍之內!
■Config.java用到了同目錄下的config.conf檔案,配置zookeepers和port應該就能用了。
可能的改進:
■HBuffer的基本類型是HashMap<String,String>,分別是列名和值。如果把HashMap<String,String>改成數組只儲存值,建立一個HRow替換HashMap<String,String>作為一行資料,列名作為HRow的靜態屬性只儲存一份,HRow維護列名和值的對應關係,這樣會省下大量列名重複佔據的記憶體並降低GC的負擔,在大資料量記憶體運算的情況下能有效提高記憶體使用量率。HRow維護對應關係佔用的CPU完全可以忽略。
■HBase讀線程和程式讀線程公用一個緩衝區,導致生產和消費使用同一個對象監視器,這樣的話notify()效率不是最優,在速度很不穩定的情況下效率可能會有小幅度降低,目前沒有解決辦法。