最近学习了HBase

HBase是什么html

最近学习了HBase,正常来讲写这篇文章,应该从DB有什么缺点,HBase如何弥补DB的缺点开始讲会更有体感,可是本文这些暂时不讲,只讲HBase,把HBase相关原理和使用讲清楚,后面有一篇文章会专门讲DB与NoSql各自的优缺点以及使用场景。mysql

HBase是谷歌Bigtable的开源版本,2006年谷歌发布《Bigtable:A Distributed Storage System For Structured Data》论文以后,Powerset公司就宣布HBase在Hadoop项目中成立,做为子项目存在。后来,在2010年左右逐渐成为Apache旗下的一个顶级项目,所以HBase名称的由来就是因为其做为Hadoop Database存在的,用于存储非结构化、半结构化的数据。redis

下图展现了HBase在Hadoop生态中的位置:sql

能够看到HBase创建在HDFS上,HBase内部管理的文件所有都是存储在HDFS中,同时MapReduce这个计算框架在HBase之上又提供了高性能的计算能力来处理海量数据。数据库

 

HBase的特色与不足apache

HBase的基本特色归纳大体以下:api

  • 海量数据存储(PB)级别,在PB级别数据以及采用廉价PC存储的状况下,数据能在几十到百毫秒内返回数据
  • 高可用,WAL + Replication机制保证集群异常不会致使写入数据丢失与数据损坏,且HBase底层使用HDFS,HDFS自己也有备份
  • 数据写入性能强劲
  • 列式存储,和传统数据库行式存储有本质的区别,这个在以后HBase存储原理的时候详细解读
  • 半结构化或非结构化数据存储
  • 存储稀松灵活,列数据为空的状况下不占据存储空间
  • 同一份数据,可存储多版本号数据,方便历史数据回溯
  • 行级别事务,能够保证行级别数据的ACID特性
  • 扩容方便,无需数据迁移,及扩即用

固然事事不是完美的,HBase也存在着如下两个最大的不足:数组

  • 没法作到条件查询,这是最大的问题,假如你的代码中存在多个查询条件,且每次使用哪一个/哪组查询条件不肯定,那么使用HBase是不合适的,除非数据冗余,设计多份RowKey
  • 作不了分页,数据总记录数几乎没法统计,由于HBase自己提供的表行数统计功能是一个MapReduce任务,极为耗时,既然拿不到总记录数,分页总署也无法肯定,天然分页也没法作了

总的来讲,对于HBase须要了解以上的一些个性应该大体上就能够了,根据HBase的特色与不足,在合适的场景下选择使用HBase,接下来针对HBase的一些知识点逐一解读。缓存

 

HBase的基本架构服务器

下图是HBase的基本架构:

从图上能够看到,HBase中包含的一些组件以下:

  • Client----包含访问HBase的接口
  • Zookeeper----经过选举保证任什么时候候集群中只有一个HMaster、HMaster与Region Server启动时向注册、存储全部Region的寻址入口、实时监控Region Server的上下线信息并实时通知给HMaster、存储HBase的Schema与Table原数据
  • HMaster----为Region Server分配Region、负责Region Server的负载均衡、发现失效的Region Server并从新分配其上的Region、管理用户对Table的增删改查
  • Region Server----维护Region并处理对Region的IO请求、切分在运行过程当中变得过大的Region

其中,Region是分布式存储和负载均衡中的最小单元,不过并非存储的最小单元。Region由一个或者多个Store组成,每一个Store保存一个列簇;每一个Store又由一个memStore和0~N个StoreFile组成,StoreFile包含HFile,StoreFile只是对HFile作了轻量级封装,底层就是HFile。

介于上图元素有点多,我这边画了一张图,把HBase架构中涉及的元素的关系理了一下:

 

HBase的基本概念

接着看一下HBase的一些基本概念,HBase是以Table(表)组织数据的,一个Table中有着如下的一些元素:

  • RowKey(行键)----即关系型数据库中的主键,它是惟一的,在HBase中这个主键能够是任意的字符串,最大长度为64K,在内部存储中会被存储为字节数组,HBase表中的数据是按照RowKey的字典序排列的。例如一、二、三、四、五、10,按照天然数的顺序是这样的,可是在HBase中1后面跟的是10而不是2,所以在设计RowKey的时候必定要充分利用字典序这个特性,将一下常常读取的行存储到一块儿或者靠近,减小Scan耗时,提升读取的效率
  • Column Family(列族)----表Schema的一部分,HBase表中的每一个列都归属于某个列族,即列族是由一系列的列组成的,必须在建立表的时候就指定好。列明都以列族做为前缀,例如courses:history、courses:math都属于courses这个列族。列族不是越多越好,过多的列族会致使io增多及分裂时数据不均匀,官方推荐列族数量为1~3个。列族不只能帮助开发者构建数据的语义边界,还能有助于开发者设置某些特性,例如能够指定某个列族内的数据压缩形式。访问控制、磁盘和内存怒的使用统计都是在列族层面进行的
  • Column(列)----通常从属于某个列族,列的数量通常没有强限制,一个列族中能够有数百万列且这些列均可以动态添加
  • Version Number(版本号)----HBase中每一列的值或者说每一个单元格的值都是具备版本号的,默认使用系统当前时间,精确到毫秒,也能够用户显式地设置。每一个单元格中,不一样版本的数据按照时间倒序排序,即最新的数据排在最前面。另外,为了不数据存在过多版本形成的管理(存储 + 索引)负担,HBase提供了两种数据版本回收的方式,一是保存数据的最后n个版本,二是保存最近一段时间内的版本,用户能够针对每一个列族进行设置
  • Cell(单元格)----一个单元格就是由RowKey、Column Family:Column、Version Number惟一肯定的,Cell中的数据是没有类型的,所有都是字节码

另一个概念就是,访问HBase Table中的行,只有三种方式:

  • 经过单个Row Key访问
  • 经过Row Key的range
  • 全表扫描

这部分介绍的Table、RowKey、Column Family、Column等都属于逻辑概念,而上部分中的Region Server、Region、Store等都属于物理概念,下图展现了逻辑概念与物理概念之间的关系:

即:table和region是一对多的关系,由于table的数据可能被打在多个region中;region和columnFamily是一对多的关系,一个store对应一个columnFamily,一个region可能对应多个store

 

HBase的逻辑表视图与物理表视图

接着看一下HBase中的表逻辑视图与物理视图。首先是逻辑表视图:

看到这里定义了2个列族,一个Personal Info、一个Family Info,对应到数据库中,至关于把两张表合并到一个一块儿。

从逻辑视图看,上图由ZhangSan、LiSi两行组成,可是在实际物理存储上却不是按照这种方式进行的存储:

看到主要是有两点差异:

  • 一行被拆开了,按照列族进行存储
  • 空列不会被存储,例如LiSi在Peronal Info中没有Provice与Phone,在Family Info中没有Brother

 

HBase的增删改查

光说不练假把式,不能光讲理论,代码也是要有的,为了方便起见,我用的是阿里云HBase,和HBase同样,只是省去了运维成本。固然虽然本人是内部员工,可是工做以外的学习是不会占用公司资源的^_^悄悄告诉你们,阿里云HBase有个福利,第一个月免费试用,想一样玩一下HBase的能够去阿里云搞一个。

首先添加一下pom依赖,用阿里云指定的HBase,使用上和原生的HBase API如出一辙:

<dependency>
    <groupId>com.aliyun.hbase</groupId>
    <artifactId>alihbase-client</artifactId>
    <version>2.0.3</version>
</dependency>
<dependency>
    <groupId>jdk.tools</groupId>
    <artifactId>jdk.tools</artifactId>
    <version>1.8</version>
    <scope>system</scope>
    <systemPath>${JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>

注意一下第二个dependency,jdk.tools不添加pom文件可能会报错"Missing artifact jdk.tools:jdk.tools:jar:1.8",错误缘由是tools.jar包是JDK自带的,pom.xml中的包隐式依赖tools.jar包,而tools.jar并未在库中,所以须要将tools.jar包添加到jdk库中。

首先写个HBaseUtil,用单例模式来写,很久没写了,顺便练习一下:

 1 /**
 2  * 五月的仓颉https://www.cnblogs.com/xrq730/p/11134806.html
 3  */
 4 public class HBaseUtil {
 5 
 6     private static HBaseUtil hBaseUtil;
 7     
 8     private Configuration config = null;
 9     
10     private Connection connection = null;
11     
12     private Map<String, Table> tableMap = new HashMap<String, Table>();
13     
14     private HBaseUtil() {
15         
16     }
17     
18     public static HBaseUtil getInstance() {
19         if (hBaseUtil == null) {
20             synchronized (HBaseUtil.class) {
21                 if (hBaseUtil == null) {
22                     hBaseUtil = new HBaseUtil();
23                 }
24             }
25         }
26         
27         return hBaseUtil;
28     }
29     
30     /**
31      * 初始化Configuration与Connection
32      */
33     public void init(String zkAddress) {
34         config = HBaseConfiguration.create();
35         config.set(HConstants.ZOOKEEPER_QUORUM, zkAddress);
36         
37         try {
38             connection = ConnectionFactory.createConnection(config);
39         } catch (IOException e) {
40             e.printStackTrace();
41             System.exit(0);
42         }
43     }
44     
45     /**
46      * 建立table
47      */
48     public void createTable(String tableName, byte[]... columnFamilies) {
49         // HBase建立表的时候必须建立指定列族
50         if (columnFamilies == null || columnFamilies.length == 0) {
51             return ;
52         }
53         
54         TableDescriptorBuilder tableDescriptorBuilder = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
55         for (byte[] columnFamily : columnFamilies) {
56             tableDescriptorBuilder.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(columnFamily).build());
57         }
58         
59         try {
60             Admin admin = connection.getAdmin();
61             admin.createTable(tableDescriptorBuilder.build());
62             // 这个Table链接存入内存中
63             tableMap.put(tableName, connection.getTable(TableName.valueOf(tableName)));
64         } catch (Exception e) {
65             e.printStackTrace();
66             System.exit(0);
67         }
68         
69     }
70     
71     public Table getTable(String tableName) {
72         Table table = tableMap.get(tableName);
73         if (table != null) {
74             return table;
75         }
76         
77         try {
78             table = connection.getTable(TableName.valueOf(tableName));
79             if (table != null) {
80                 // table对象存入内存
81                 tableMap.put(tableName, table);
82             }
83             
84             return table;
85         } catch (IOException e) {
86             e.printStackTrace();
87             return null;
88         }
89     }
90     
91 }

注意,HBase中的数据一切皆二进制,所以从上面代码到后面代码,字符串所有都转换成了二进制。

接着定义一个BaseHBaseUtilTest类,把一些基本的定义放在里面,保持主测试类清晰:

 1 /**
 2  * 五月的仓颉https://www.cnblogs.com/xrq730/p/11134806.html
 3  */
 4 public class BaseHBaseUtilTest {
 5 
 6     protected static final String TABLE_NAME = "student";
 7     
 8     protected static final byte[] COLUMN_FAMILY_PERSONAL_INFO = "personalInfo".getBytes();
 9     
10     protected static final byte[] COLUMN_FAMILY_FAMILY_INFO = "familyInfo".getBytes();
11     
12     protected static final byte[] COLUMN_NAME = "name".getBytes();
13     
14     protected static final byte[] COLUMN_AGE = "age".getBytes();
15     
16     protected static final byte[] COLUMN_PHONE = "phone".getBytes();
17     
18     protected static final byte[] COLUMN_FATHER = "father".getBytes();
19     
20     protected static final byte[] COLUMN_MOTHER = "mother".getBytes();
21     
22     protected HBaseUtil hBaseUtil;
23     
24 }

第一件事情,建立Table,注意前面说的,HBase必须Table和列族一块儿建立:

 1 /**
 2  * 五月的仓颉https://www.cnblogs.com/xrq730/p/11134806.html
 3  */
 4 public class HBaseUtilTest extends BaseHBaseUtilTest {
 5 
 6     @Before
 7     public void init() {
 8         hBaseUtil = HBaseUtil.getInstance();
 9         hBaseUtil.init("xxx");
10     }
11     
12     /**
13      * 建立表
14      */
15     @Test
16     public void testCreateTable() {
17         hBaseUtil.createTable(TABLE_NAME, COLUMN_FAMILY_PERSONAL_INFO, COLUMN_FAMILY_FAMILY_INFO);
18     }
19     
20 }

我本身申请的HBase,zk地址就不给你们看啦,若是一样申请了的,替换一下就行了。testCreateTable方法运行一下,就建立好了student表。接着利用put建立四条数据,多建立几条,等下scan能够测试:

 1 /**
 2  * 添加数据
 3  */
 4 @Test
 5 public void testPut() throws Exception {
 6     Table table = hBaseUtil.getTable(TABLE_NAME);
 7     // 用户1,用户id:12345
 8     Put put1 = new Put("12345".getBytes());
 9     put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Lucy".getBytes());
10     put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "18".getBytes());
11     put1.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13511112222".getBytes());
12     put1.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "LucyFather".getBytes());
13     put1.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "LucyMother".getBytes());
14     // 用户2,用户id:12346
15     Put put2 = new Put("12346".getBytes());
16     put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Lily".getBytes());
17     put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "19".getBytes());
18     put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13522223333".getBytes());
19     put2.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "LilyFather".getBytes());
20     put2.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "LilyMother".getBytes());
21     // 用户3,用户id:12347
22     Put put3 = new Put("12347".getBytes());
23     put3.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "James".getBytes());
24     put3.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "22".getBytes());
25     put3.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER, "JamesFather".getBytes());
26     put3.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "JamesMother".getBytes());
27     // 用户4,用户id:12447
28     Put put4 = new Put("12447".getBytes());
29     put4.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME, "Micheal".getBytes());
30     put4.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, "22".getBytes());
31     put2.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE, "13533334444".getBytes());
32     put4.addColumn(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER, "MichealMother".getBytes());
33     
34     table.put(Lists.newArrayList(put1, put2, put3, put4));
35 }

一样的,运行一下testPut方法,四条数据就建立完毕了。注意为了提高处理效率,HBase的get、put这些API都提供的批量处理方式,这样一次提交能够提交多条数据,发起一次请求便可,不用发起请求。

接着看一下利用Get API查询数据:

 1 /**
 2  * 获取数据
 3  */
 4 @Test
 5 public void testGet() throws Exception {
 6     Table table = hBaseUtil.getTable(TABLE_NAME);
 7     // get1,拿到所有数据
 8     Get get1 = new Get("12345".getBytes());
 9     // get2,只拿personalInfo数据
10     Get get2 = new Get("12346".getBytes());
11     get2.addFamily(COLUMN_FAMILY_PERSONAL_INFO);
12         
13     Result[] results = table.get(Lists.newArrayList(get1, get2));
14     if (results == null || results.length == 0) {
15         return ;
16     }
17         
18     for (Result result : results) {
19         printResult(result);
20     }
21 }
22 
23 private void printResult(Result result) {
24     System.out.println("====================分隔符====================");
25     printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_NAME));
26     printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE));
27     printBytes(result.getValue(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_PHONE));
28     printBytes(result.getValue(COLUMN_FAMILY_FAMILY_INFO, COLUMN_FATHER));
29     printBytes(result.getValue(COLUMN_FAMILY_FAMILY_INFO, COLUMN_MOTHER));
30 }
31     
32 private void printBytes(byte[] bytes) {
33     if (bytes != null && bytes.length != 0) {
34         System.out.println(new String(bytes));
35     }
36 }

HBase查询数据比较灵活的是,能够查询RowKey下对应的全部数据、能够按照RowKey-Column Family的维度查询数据、能够按照RowKey-Column Family-Column的维度查询数据,也能够按照RowKey-Column Family-Column-Timestamp的维度查询数据,能够查询Timestamp区间内的数据,也能够查询RowKey-Column Family-Column下全部Timestamp数据。上面的代码执行结果为:

====================分隔符====================
Lucy
18
13511112222
LucyFather
LucyMother
====================分隔符====================
Lily
19
13533334444

和咱们的预期相符,即"12345"这个RowKey查询出了全部数据,"12346"这个RowKey只查了personalInfo这个列族的数据。

最后这一部分咱们看一下更新,更新的API和新增的API都是同样的,都是Put:

@Test
public void testUpdate() throws Exception {
    Table table = hBaseUtil.getTable(TABLE_NAME);
    // 用户1,用户id:12345
    Put put = new Put("12346".getBytes());
    put.addColumn(COLUMN_FAMILY_PERSONAL_INFO, COLUMN_AGE, 1, "22".getBytes());
    table.put(put);
}

Get看一下执行12346这条数据的值:

Lily
19
13533334444

看到12346对应的数据,本来Age是19,更新到22,依然是19,这就是一个值得注意的点了。HBase的更新实际上是往Table里面新增一条记录,按照Timestamp进行排序,最新的数据在前面,每次Get的时候将第一条数据取出来。在这里咱们指定的Timestamp=1,这个值落后于先前插入的Timestamp,天然就排在后面,所以读取出来的Age依然是原值19,这个细节特别注意一下。

 

HBase的Scan

感受前面篇幅有点大,因此这里专门抽一个篇幅出来写一下Scan,Scan是HBase扫描数据的方式。

首先能够看一下最基本的Scan:

 1 /**
 2  * 扫描
 3  */
 4 @Test
 5 public void testScan() throws Exception {
 6     Table table = hBaseUtil.getTable(TABLE_NAME);
 7     Scan scan = new Scan().withStartRow("12345".getBytes(), true).withStopRow("12347".getBytes(), true);
 8         
 9     ResultScanner rs = table.getScanner(scan);
10     if (rs != null) {
11         for (Result result : rs) {
12             printResult(result);
13         }
14     }
15 }

执行结果为:

====================分隔符====================
Lucy
19
13511112222
LucyFather
LucyMother
====================分隔符====================
Lily
19
13533334444
LilyFather
LilyMother
====================分隔符====================
James
22
JamesFather
JamesMother

表示查询12345~12347这个范围内的全部RowKey,withStartRow的第二个参数true表示包含,若是为false那么12345这个RowKey就查不出来了。

进阶的,HBase为咱们提供了带过滤器的Scan,一共有十来种,我这边只演示两种以及组合的状况,其余的查询一下HBase API文档便可,2.1版本的API文档地址为http://hbase.apache.org/2.1/apidocs/index.html。演示代码以下:

 1 @Test
 2 public void testScanFilter() throws Exception {
 3     Table table = hBaseUtil.getTable(TABLE_NAME);
 4         
 5     System.out.println("********************RowFilter测试********************");
 6     Scan scan0 = new Scan().withStartRow("12345".getBytes(), true);
 7     scan0.setFilter(new RowFilter(CompareOperator.EQUAL, new BinaryComparator("12346".getBytes())));
 8     ResultScanner rs0 = table.getScanner(scan0);
 9     printResultScanner(rs0);
10         
11     System.out.println("********************PrefixFilter测试********************");
12     Scan scan1 = new Scan().withStartRow("12345".getBytes(), true);
13     scan1.setFilter(new PrefixFilter("124".getBytes()));
14     ResultScanner rs1 = table.getScanner(scan1);
15     printResultScanner(rs1);
16         
17     System.out.println("********************两种Filter同时知足测试********************");
18     Scan scan2 = new Scan().withStartRow("12345".getBytes(), true);
19     Filter filter0 = new RowFilter(CompareOperator.EQUAL, new BinaryComparator("12447".getBytes()));
20     Filter filter1 = new PrefixFilter("124".getBytes());
21     FilterList filterList = new FilterList(FilterList.Operator.MUST_PASS_ALL, filter0, filter1);
22     scan2.setFilter(filterList);
23     ResultScanner rs2 = table.getScanner(scan2);
24     printResultScanner(rs2);
25 }

执行结果为:

********************RowFilter测试********************
====================分隔符====================
Lily
19
13533334444
LilyFather
LilyMother
********************PrefixFilter测试********************
====================分隔符====================
Micheal
22
MichealMother
********************两种Filter同时知足测试********************
====================分隔符====================
Micheal
22
MichealMother

总的来讲,HBase本质上是KV型NoSql,根据Key查询Value是最高效的,Scan这个API仍是慎用,范围里面的数据量小倒无所谓,一旦RowKey设计不合理,StartRow和EndRow没有指定好,可能会形成大范围的扫描,下降HBase总体能力。

 

HBase和KV型缓存的区别

看了上面的代码演示,不知道你们有没有和我一开始有同样的疑问:HBase看上去也是K-V形式的,那么它和支持KV型数据的缓存(例如Redis、MemCache、Tair)有什么区别?

我用一张表格总结一下两者的区别:

总的来讲,一样做为数据库的NoSql替代方案,HBase更加适合用于海量数据的持久化场景,KV型缓存更加适合用于对数据的高性能读写上。

 

HBase的Region分裂及会致使的热点问题

经典问题,首先看一下什么是Region分裂,只把Region分裂讲清楚,不讲具体Region分裂的实现方式,理由也很简单,Region分裂细节学得再清楚,对工做中的帮助也不大,不必太过于追根究底。

Region分裂是HBase可以拥有良好扩张性的最重要因素之一,也必然是全部分布式系统追求无限扩展性的一副良药。经过前面的部分咱们知道HBase的数据以行为单位存储在HBase表中,HBase表按照多行被分割为多个Region,这个Region分布在HBase集群中,而且由Region Server进程负责讲这些Region提供给Client访问。一个Region中,RowKey是一个连续的范围,也就是说表中的记录在Region中是按照startKey到endKey的范围为RowKey进行排序存储的。一般一个表由多个Region构成,这些Region分布在多个Region Server上,也就是说,Region是在Region Server中插入和查询数据时负载均衡的物理机制。一张HBase表在刚刚建立的时候默认只有一个Region,因此关于这张表的请求都被路由到同一个Region Server,不管集群中有多少Region Server,而一旦某个Region的大小达到必定值,就会自动分裂为两个Region,这也就是为何HBase表在刚刚建立的阶段不能充分利用整个集群吞吐量的缘由。

在HBase管理界面能够查看每一个Region,startKey与endKey的范围,例如(图片来自网络):

这里特别注意一个点,RowKey是按照Key的字符天然顺序进行排序的,所以RowKey=9的Key,会落在最后一个Region Server中而不是第一个Region Server中

那么什么是热点问题应该也很好理解了:

虽然HBase的单机读写性能强劲,可是当集群中成千上万的请求RowKey都落在aaaaa-ddddd之间,那么这成千上万请求最终落到Region Server1这台服务器上,一旦超出服务器自身承受能力,那么必然致使服务器不可用甚至宕机。所以咱们说设计RowKey的时候千万把时间戳或者id自增的方式做为RowKey方案就是这个道理,时间戳或者id自增的方式,虽然最终可让RowKey落到不一样的Region中,可是在当下或者当下日后的一段时间内,RowKey必定是会落到同一个Region中的,数据热点问题将严重影响HBase集群能力。

解决热点问题一般有两个方案,最初级的方案是设置预分区,即在Table建立的时候就先设置几个Region,为每一个Region划分不一样的startKey与endKey,但这么作有如下两个缺点:

  • 高度依赖RowKey,必须事先知道插入数据的RowKey的分布
  • 即便事先知道插入数据的RowKey分布,可是若是数据分布不均匀或者存在热点行,依然没法均匀分摊负载

可是不管如何,设置预分区依然是一种解决热点问题的方案。

第二个解决方案是一劳永逸的解决方案也是使用HBase最核心的一个点:合理设计RowKey。即让RowKey均匀分布在Region中,大体有如下几个方案可供参考:

  • 倒序。例如手机号码135ABCD、135EFGH、135IJKL这种,前缀没有区分度,很是容易落到相同的Region中,此时作倒序即DCBA53一、HGFE53一、LKJI531,将有区分度的部分放在前面,就很是容易将数据散落在不一样的Region中
  • 原数据加密。例如作MD5,由于MD5的随机性是很是强的,所以作了MD5后,数据将会很是分散
  • 加随机前缀。例如ASCII码中随机选5位做为数据前缀,一样能够达到分散RowKey的效果,可是缺点是必须记住每一个原数据对应的前缀

不管如何,仍是那句话,合理设计RowKey是HBase使用的核心。

 

WAL机制

最后讲一下前面提到的WAL机制,WAL的全称为Write Ahead Log,它是HBase的RegionServer在处理数据插入和删除的过程当中用来记录操做内容的一种日志,是用来作灾难恢复的。

其实WAL并非什么新鲜思想,在数据库领域很常见:

  • mysql有binlog,记录每一次数据变动
  • redis有aof,在开启aof的状况下,每隔短暂时间,将这段时间产生的操做记录文件

其核心都是,变动数据前先写磁盘日志文件,在系统发生异常的时候,重放日志文件对数据进行恢复,HBase的WAL机制也是同样的思想,数据变动步骤为:

  • 首先从以前的图上能够看到有HLog,HLog是实现WAL的类,一个RegionServer对应一个HLog,多个Region共享一个HLog,不过从HBase1.0版本开始能够定义多个HLog以提升吞吐量
  • 客户端的一次数据提交先写HLog,这个是告知客户端数据提交成功的前提
  • HLog写入成功后写入MemStore
  • 当MemStore的值达到必定程度后,flush到hdfs,造成一个一个的StoreFile(HFile)
  • flush事后,HLog中对应的数据就没用了,删除

由于有了HLog,即便在MemStore中的数据尚未flush到hdfs的时候系统发生了宕机或者重启,数据都不会出现丢失。

相关文章
相关标签/搜索