HBase应用开发规则
Configuration实例的创建
该类应该通过调用HBaseConfiguration的create()方法来实例化。否则,将无法正确加载HBase中的相关配置项。
正确示例:
//该部分,应该是在类成员变量的声明区域声明 private Configuration hbaseConfig = null; //建议在类的构造函数中,或者初始化方法中实例化该类 hbaseConfig = HBaseConfiguration.create();
错误示例:
hbaseConfig = new Configuration();
共享Configuration实例
HBase客户端代码通过创建一个与ZooKeeper之间的HConnection,来获取与一个HBase集群进行交互的权限。一个ZooKeeper的HConnection连接,对应着一个Configuration实例,已经创建的HConnection实例,会被缓存起来。也就是说,如果客户端需要与HBase集群进行交互的时候,会传递一个Configuration实例到缓存中去,HBase Client部分通过已缓存的HConnection实例,来判断属于这个Configuration实例的HConnection实例是否存在,如果不存在,会创建一个新的HConnection,如果存在,则会直接返回相应的实例。
因此,如果频繁地创建Configuration实例,会导致创建很多不必要的HConnection实例,很容易达到ZooKeeper的连接数上限。
建议在整个客户端代码范围内,都共用同一个Configuration对象实例。
Table实例的创建
public abstract class TableOperationImpl { private static Configuration conf = null; private static Connection connection = null; private static Table table = null; private static TableName tableName = TableName.valueOf("sample_table"); public TableOperationImpl() { init(); } public void init() { conf = ConfigurationSample.getConfiguration(); try { connection = ConnectionFactory.createConnection(conf); table = conn.getTable(tableName); } catch (IOException e) { e.printStackTrace(); } } public void close() { if (table != null) { try { table.close(); } catch (IOException e) { System.out.println("Can not close table."); } finally { table = null; } } if (connection != null) { try { connection.close(); } catch (IOException e) { System.out.println("Can not close connection."); } finally { connection = null; } } } public void operate() { init(); process(); close(); } }
不允许多个线程在同一时间共用同一个Table实例
Table是一个非线程安全类,因此,同一个Table实例,不应该被多个线程同时使用,否则可能会出现并发问题。
Table实例缓存
如果一个Table实例可能长时间会被同一个线程固定且频繁地用到,例如,通过一个线程不断地往一个表内写入数据,那么这个Table在实例化后,就需要缓存下来,而不是每一次插入操作,都要实例化一个Table对象(尽管提倡实例缓存,但也不是在一个线程中一直沿用一个实例,个别场景下依然需要重构,可参见下一条规则)。
正确示例:
注意该实例中提供的以Map形式缓存Table实例的方法,未必通用。这与多线程多Table实例的设计方案有关。如果确定一个Table实例仅仅可能会被用于一个线程,而且该线程也仅有一个Table实例的话,就无须使用Map。这里提供的思路仅供参考。
//该Map中以TableName为Key值,缓存所有已经实例化的Table private Map<String, Table> demoTables = new HashMap<String, Table>(); //所有的Table实例,都将共享这个Configuration实例 private Configuration demoConf = null; /** * <初始化一个HTable类> * <功能详细描述> * @param tableName * @return * @throws IOException * @see [类、类#方法、类#成员] */ private Table initNewTable(String tableName) throws IOException { try (Connection conn = ConnectionFactory.createConnection(demoConf)){ return conn.getTable(tableName); } } /** * <获取Table实例> * <功能详细描述> * @see [类、类#方法、类#成员] */ private Table getTable(String tableName) { if (demoTables.containsKey(tableName)) { return demoTables.get(tableName); } else { Table table = null; try { table = initNewTable(tableName); demoTables.put(tableName, table); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } return table; } } /** * <写数据> * <这里未涉及到多线程多Table实例在设计模式上的优化.这里采用同步方法, * 主要是是考虑到同一个Table是非线程安全的.通常,建议一个Table实例,在同一 * 时间只能被用在一个写数据的线程中> * @param dataList * @param tableName * @see [类、类#方法、类#成员] */ public void putData(List<Put> dataList, String tableName) {Table table = getTable(tableName); //关于这里的同步:如果在采用的设计方案中,不存在多线程共用同一个Table实例 //的可能的话,就无须同步了。这里需要注意Table实例是非线程安全的 synchronized (table) { try { table.put(dataList); table.notifyAll(); } catch (IOException e) { // 在捕获到IOE时,需要将缓存的实例重构。 try { // 关闭之前的Connection. table.close(); // 重新创建这个实例. table = initNewTable(tableName); } catch (IOException e1) { // TODO } } } }
错误示例:
public void putDataIncorrect(List<Put> dataList, String tableName) {Table table = null; try { //每次写数据,都创建一个HTable实例 table = initNewTable(tableName); table.put(dataList); } catch (IOException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } finally { table.close(); } }
Table实例写数据的异常处理
尽管在前一条规则中提到了提倡Table实例的重构,但是,并非提倡一个线程自始至终要沿用同一个Table实例,当捕获到IOException时,依然需要重构Table实例。示例代码可参考上一个规则的示例。
另外,请谨慎调用如下两个方法:
- Configuration#clear:
这个方法,会清理所有已加载的属性,对于已经在使用这个Configuration的类或线程而言,可能会带来潜在的问题(例如,假如Table还在使用这个Configuration,那么,调用这个方法后,Table中的这个Configuration的所有的参数,都被清理掉了),也就是说:只要还有对象或者线程在使用这个Configuration,就不应该调用这个clear方法,除非所有的类或线程,都已经确定不用这个Configuration了。
因此,这个方法,应该要放在进程退出时执行,而不是每一次Table要重构的时候执行。
- HConnectionManager#deleteAllConnections:
这个可能会导致现有的正在使用的连接被从连接集合中清理掉,同时,因为在HTable中保存了原有连接的引用,可能会导致这个连接无法关闭,进而可能会造成泄漏。因此,这个方法不建议使用。
写入失败的数据要做相应的处理
在写数据的过程中,如果进程异常或一些其它的短暂的异常,可能会导致一些写入操作失败。因此,对于操作的数据,需要将其记录下来。在集群恢复正常后,重新将其写入到HBase数据表中。
另外,有一点需要注意:HBase Client返回写入失败的数据,是不会自动重试的,仅仅会告诉接口调用者哪些数据写入失败了。对于写入失败的数据,一定要做一些安全的处理,例如可以考虑将这些失败的数据,暂时写在文件中,或者,直接缓存在内存中。
正确示例:
private List<Row> errorList = new ArrayList<Row>(); /** * <采用PutList的模式插入数据> * <如果不是多线程调用该方法,可不采用同步> * @param put 一条数据记录 * @throws IOException * @see [类、类#方法、类#成员] */ public synchronized void putData(Put put) { // 暂时将数据缓存在该List中 dataList.add(put); // 当dataList的大小达到PUT_LIST_SIZE之后,就执行一次Put操作 if (dataList.size() >= PUT_LIST_SIZE) { try { demoTable.put(dataList); } catch (IOException e) { // 如果是RetriesExhaustedWithDetailsException类型的异常, // 说明这些数据中有部分是写入失败的这通常都是因为 // HBase集群的进程异常引起,有时也会因为有大量 // 的Region正在被转移,导致尝试一定的次数后失败 if (e instanceof RetriesExhaustedWithDetailsException) { RetriesExhaustedWithDetailsException ree = (RetriesExhaustedWithDetailsException)e; int failures = ree.getNumExceptions(); for (int i = 0; i < failures; i++) { errorList.add(ree.getRow(i)); } } } dataList.clear(); } }
资源释放
关于ResultScanner和Table实例,在用完之后,需要调用它们的Close方法,将资源释放掉。Close方法,要放在finally块中,来确保一定会被调用到。
正确示例:
ResultScanner scanner = null; try { scanner = demoTable.getScanner(s); //Do Something here. } finally { scanner.close(); }
错误示例:
- 在代码中未调用scanner.close()方法释放相关资源。
- scanner.close()方法未放置在finally块中。
ResultScanner scanner = null; scanner = demoTable.getScanner(s); //Do Something here. scanner.close();
Scan时的容错处理
Scan时不排除会遇到异常,例如,租约过期。在遇到异常时,建议Scan应该有重试的操作。
事实上,重试在各类异常的容错处理中,都是一种优秀的实践,这一点,可以应用在各类与HBase操作相关的接口方法的容错处理过程中。
不用Admin时,要及时关闭,Admin实例不应常驻内存
Admin的实例应尽量遵循 “用时创建,用完关闭”的原则。不应该长时间缓存同一个Admin实例。