Java多线程是一个很麻烦的东西,为了简化开发加快速度封装了HBase多线程操作,包括常用的Scan,Get,Put,Delete四种操作。经过多次修改运行非常稳定,已经用于生产环境。内部线程通信使用wait()/notify()机制,效率很高。本文只在Java层面讨论HBase的多线程,HBase API内部多线程机制不在本文讨论范围之内。HBase客户端需要的jar包自行下载,这里就不提供了,本类源码结尾处可以下载。下面通过demo演示一下用法,结尾处有代码打包下载。
特点:
- 从Java层面用多线程最大化了读写性能。
- 大量减少了应用程序的代码量,现在可以集中精力到数据分析上了。
- 性能平均下来至少比单线程高出n倍。
HScan
HScan有5个public方法:
- public HScan(String table, String[] columns, int threadNum),table指定表名,columns指定扫描的列,如果设为null代表扫描所有的列,如果设为空数组代表只扫描rowkey,threadNum设置扫描器的并发数。
- public void addTask(String start, String end, int num),前两个参数是起止rowkey,第三个参数用于控制rowkey平分成num段,这个参数主用用于可整型计算的时间戳开头的rowkey,如果非整型范围内数字这个参数设为1即可。这个方法可以调用多次,每次添加num个任务到任务池。
- public void (),继承来的方法,开始进行扫描。
- public HashMap<String, String> fetch(),从缓冲区里取一个结果出来,结果中的键由列名和”rowKey”组成,如果返回null表示所有行都已经读完了。
- public void status(),一个非同步方法,用于近似显示线程的运行状态,总是在当前行显示,内容由4部分组成,第一列显示已经返回的结果数,第二列显示缓冲区中的行数,第三列分别表示 当前并发线程数/Thread.State.WAITING数/Thread.State.BLOCKED数,第四列显示任务池中的任务数。
Demo,扫描gm_player_detail表2013年1月1日到10日的所有数据
01 package joyport.hbase.gm;
02
03 import java.util.HashMap;
04
05 public class Test {
06 private static int threadNum = 30;
07 private static int taskNum = 1000;
08 private static String htable = "gm_player_detail";
09
10 public static void main(String[] args) throws Exception {
11 int[] time = new int[2];
12 if (args.length == 2) {
13 time = Util.getTimeScale(args[0], args[1]);
14 } else if (args.length == 1) {
15 time = Util.getTimeScale(-1);
16 time[0] = Util.getTimeScale(args[0], args[0])[0];
17 } else {
18 time = Util.getTimeScale(-1);
19 }
20 HScan hScan = new HScan(htable, null, threadNum);
21 hScan.addTask(String.valueOf(time[0]), String.valueOf(time[1]), taskNum);
22 hScan.();
23 Test test = new Test();
24 test.analyse(hScan);
25 }
26
27 public void analyse(HScan hScan) throws InterruptedException {
28 HashMap<String, String> row = null;
29 for (row = hScan.fetch(); row != null; row = hScan.fetch()) {
30 hScan.status();
31 //System.out.println(row);
32 }
33 }
34 }
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获取所有列。
01 package joyport.hbase.gm;
02
03 import java.util.HashMap;
04
05 public class Test {
06 private static int threadNum = 30;
07 private static int taskNum = 1000;
08 private static String htable = "gm_player_detail";
09
10 public static void main(String[] args) throws Exception {
11 int[] time = new int[2];
12 if (args.length == 2) {
13 time = Util.getTimeScale(args[0], args[1]);
14 } else if (args.length == 1) {
15 time = Util.getTimeScale(-1);
16 time[0] = Util.getTimeScale(args[0], args[0])[0];
17 } else {
18 time = Util.getTimeScale(-1);
19 }
20 String[] cols = {};
21 HScan hScan = new HScan(htable, cols, threadNum);
22 hScan.addTask(String.valueOf(time[0]), String.valueOf(time[1]), taskNum);
23 hScan.();
24 Test test = new Test();
25 test.analyse(hScan);
26 }
27
28 public void analyse(HScan hScan) throws Exception {
29 HashMap<String, String> row = null;
30 HGet hGet = new HGet(htable, null, 30);
31 for (row = hScan.fetch(); row != null; row = hScan.fetch()) {
32 hScan.status();
33 //System.out.println(row);
34 hGet.addTask(row.get("rowKey"));
35 }
36 hGet.();
37 for (row = hGet.fetch(); row != null; row = hGet.fetch()) {
38 hScan.status();
39 //System.out.println(row);
40 }
41 }
42 }
HPut
HPut和HScan,HGet不太一样,因为put操作的数据源可以有多个,而且类型可以任意(从文本,数据库,HBase表,流),所以对数据源线程也进行了封装,数据源线程数由用户决定。
- public HPut(String table, int threadNum) threadNum是写入线程的数目。
- public void addTask(final Callable<HashMap<String, String>> task) 返回一行记录的接口,每调用一次产生一个新的数据源线程,当返回null的时候数据源线程结束。
- public void enableStatus(boolean enable),即使是一个近似的状态也需要在合适的地方显示,用户程序中显示基本上误差非常大,所以放到put操作的时候显示,这个函数用来控制是否显示put状态,默认显示。
Demo,只展示一个数据源的情况,多数据源没有测试(理论上应该没问题)。从gm_player_detail读取一天的数据插入到hbase_test表。
01 package joyport.hbase.gm;
02
03 import java.util.HashMap;
04 import java.util.concurrent.Callable;
05
06 public class Test extends Thread implements Callable<HashMap<String, String>> {
07 private int threadNum = 30;
08 private int taskNum = 1000;
09 private String htable = "gm_player_detail";
10 private HScan hScan;
11
12 public static void main(String[] args) throws Exception {
13 int[] time = new int[2];
14 if (args.length == 2) {
15 time = Util.getTimeScale(args[0], args[1]);
16 } else if (args.length == 1) {
17 time = Util.getTimeScale(-1);
18 time[0] = Util.getTimeScale(args[0], args[0])[0];
19 } else {
20 time = Util.getTimeScale(-1);
21 }
22 Test t1 = new Test(time[0], time[1]);
23 // 往hbase_test写数据
24 HPut hPut = new HPut("hbase_test", 10);
25 hPut.addTask(t1);
26 hPut.();
27 }
28
29 public Test(int startkey, int endkey) throws Exception {
30 String[] cols = null;
31 hScan = new HScan(htable, cols, threadNum);
32 hScan.addTask(String.valueOf(startkey), String.valueOf(endkey), taskNum);
33 hScan.();
34 }
35
36 public HashMap<String, String> call() throws InterruptedException {
37 return hScan.fetch();
38 }
39 }
HDelete
delete操作只需要rowkey就可以,但是缓冲区数据结构是死的,所以也使用HashMap<String,String>格式,以rowKey为键。如果row.isEmpty()为真则这条数据跳过。
Demo,从hbase_test读取所有行,并删除所有行。
01 package joyport.hbase.gm;
02
03 import java.util.HashMap;
04 import java.util.concurrent.Callable;
05
06 public class Test implements Callable<HashMap<String, String>> {
07 private static int threadNum = 30;
08 private static String htable = "hbase_test";
09 private static HScan hScan;
10
11 public static void main(String[] args) throws Exception {
12 String[] cols = {};
13 hScan = new HScan(htable, cols, threadNum);
14 hScan.addTask("0", "2", 1);
15 hScan.();
16
17 HDelete h = new HDelete(htable, 10);
18 h.addTask(new Test());
19 h.();
20 }
21
22 public HashMap<String, String> call() throws Exception {
23 return hScan.fetch();
24 }
25 }
需要注意的问题:
- 默认缓冲区大小是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()效率不是最优,在速度很不稳定的情况下效率可能会有非常小幅度的降低,目前没有解决办法。