概述

HBase和传统的数据库有很大的不同之处,比如MySQL,PostGreSQL,Oracle等。在架构和提供的特性方面都有不同之处,HBase去掉了一些伸缩和灵活性的特性,这也就使得hbase拥有一个非常不同的数据模型。设计hbase的表和传统关系数据库非常不同。我会通过解释hbase数据模型以及通过一些实例来介绍hbase表的基本设计。

Hbase数据模型

hbase数据模型和关系型数据库是非常不同的。就像Bigtable描述的那样,这是一个稀疏的,分布式的,持久化的,多维的,排序的映射,索引通过行键,列键以及时间戳来实现。你可能听别人把它当做一个key-value存储结构的,或者说是面向列族的数据库。或者说是多版本映射的数据库。所有的这些描述都是正确的。这个章节,我们介绍一下这些概念。

hbase数据模型最让人可以接收的描述就是使用表,行和列。这和关系型数据库很像。但是也就是名称相似而已,行列的概念还是略有不同的,下面我们介绍一下这些概念。

表(Table)

hbase在表中组织数据。表名是字符串和字符的组合,可以在文件系统路径中使用。

行(Row)

在表中数据依赖于行来存储,行通过行键来区分。行键没有数据类型,通常是一个字节数组。

列族(Column Family)

行中的数据通过列族来组织。列族也暗示了数据的物理排列。所以列族必须预先定义,并且不容易被修改。每行都拥有相同的列族,可能有些行的数据为空。列族是字符串和字符的组合,可以在文件系统路径
中使用。

列标识(Column Qualifier)

数据在列族中的位置是通过列标识来指定的。列标识不需要预先指定,每行的列标识也不需要相同。就像行键一样,列标识没有数据类型,通常也是字节数组。

单元(Cell)

单元是行键、列族、列标识的组合。这些数据存储在单元中,被称作单元数据。数据也不需要数据类型,通常也是字节数组。

时间戳(Timestamp)

单元数据是有版本的。版本的区分就是他们的版本号,版本号默认就是时间戳。当写入数据时,如果没有指定时间,那么默认的时间就是系统的当前时间。

读取数据的时候,如果没有指定时间,那么返回的就是最新的数据。

保留版本的数量根据每个列族的配置。默认的版本数量是3。

hbase中的一个表就像下面这张图:

hbase表设计TTl hbase表设计 画像_元数据

在这个图中,表包含两个列族,personal和office。每个列族都有两列。每个方格就是一个单元,行键根据字母顺序进行排序。
hbase的API数据管理包含三个主要方法:get,put和scan。get和put方法需要制定行键,scan操作是浏览一定范围的行。范围可以通过开始和结束的行键来指定。如果不指定那么就是浏览整个表数据。

有时候,使用多维映射来理解数据模型可能更简单。多维映射就像下图所示:

hbase表设计TTl hbase表设计 画像_数据_02

行键映射一个列族的列表,列族映射一个列标识的列表,列标识映射一个时间戳的列表,每个时间戳映射一个值,也就是单元值。

如果你使用行键来检索映射的数据,那么你会得到所有的列。如果你检索特定列族的数据,你会得到此列族下所有的列标识。

如果你检索列标识所映射的数据,你会得到所有的时间戳以及对应的数据。hbase优化了返回数据,默认仅仅返回最新版本的数据。

当然你也可以得多一个多版本返回。行键和关系数据库中的主键有相同的作用,你不能改变列的行键,换句话说就是,如果表中已经插入数据,那么personal列族中的列名不能改变它所属的行键。

就像之前提到的,你可以使用不同的方式来理解数据模型。当然你也可以使用键值的方式来理解,键就是行键,值就是列中的值,但是给定一个行键仅仅能确定一行的数据。

你可以把行键,列族,列标识,时间戳都看做键。而值就是单元中的数据。当你深入入到存储层的时候,你会看到如果你想要读取一个特定单元数据的时候,你就会先得到一个数据库块,这个块也会包含其他单元的数据。

下面是键值结构图:

hbase表设计TTl hbase表设计 画像_hbase表设计TTl_03

HBase表设计基础

之前的章节已经介绍,hbase的数据模型和关系型数据库是十分不同的。那么hbase的表设计就和关系数据库表设计有很大不同之处。设计hbase表需要回答下面的问题:

  • 1.行键的结构是什么的并且要包含什么内容?
  • 2.表有多少个列族?
  • 3.列族中都要放什么数据?
  • 4.每个列族中有多少个列?
  • 5.列名是什么?尽管列名在创建表时不需要指定,你读写数据是需要用到它们。
  • 6.单元数据需要包含哪些信息?
  • 7.每个单元数据需要存储的版本数量是多少?

定义hbase表最重要的事情就是行键的结构。为了更有效的定义,首先定义访问模式是很重要的。为了定义表的结构,一些hbase特定的属性是需要考虑在内的,如下所示:

  • 1.索引仅仅依赖于Key
  • 2.表数据根据行键排序,表中的每个区域都代表了一部分行键的空间,这个区域通过开始和结束行键来指定
  • 3.hbase表中的数据都是字节数组,没有类型之分。
  • 4.原子性仅保证在行级。跨行操作不保证原子性,也就是说不存在多行事务。
  • 5.列族必须在表创建的时候就定义。
  • 6.列标识是动态的,可以在写入数据是定义。

一个好的方法去学习这些感念就是通过一个例子。下面我们使用hbase表来设计推特的用户关系(用户关注另一个用户)。关注和被关注的关系实际上就是图,使用指定的图数据库可能工作的更加高效。然而,这里只是一个使用用例来理解hbase的一些概念。

首先我们来设计表的访问模式。访问模式定义如下:

读模式:

  • 1.用户关注了谁?
  • 2.是否用户A关注了用户B?
  • 3.都有谁关注了用户A?

写模式:

  • 1.用户关注了一个新的用户。
  • 2.用户取消关注。

下面我们使用递进式的方法考虑几种表设计,并看她们的优缺点。第一种设计如下:

hbase表设计TTl hbase表设计 画像_数据_04

在一行中存储被关注的用户的列表(也就是此用户都关注了谁),行键是用户ID。每一列包含关注的用户ID。表中的数据如下:

hbase表设计TTl hbase表设计 画像_数据_05

这个表设计满足读模式的1和2。但是要想获得都有谁关注了此用户是非常耗时的,需要遍历整个表。

添加一个关注的用户也是很棘手的,因为没有一个计数器来表示添加的用户是第几个,除非你遍历之前添加的所有的用户,这也有些不合理。

一种可用的解决方法就是使用一个额外的列来保存一个计数器。如下图所示:

hbase表设计TTl hbase表设计 画像_数据模型_06

当添加关注用户的时候,要实时的更新count的值。
这个设计比最早的设计要好一点,但不能解决所有的问题。取消关注也是棘手的,因为你要遍历整行的数据来找到具体要删除的用户。而且删除之后计数器将会产生漏洞。

我们之前提到过,列标识是动态的,以字节数组的方式存储。这就可以让你使用任意的数据来代表列标识。考虑一下下面的设计,在这个设计中,数量不是必须的。所以添加用户变得很简单,取消关注也是很简单的。单元数据可以存储任意的值对结果都没有影响。

hbase表设计TTl hbase表设计 画像_数据_07

这个设计解决了定义的大部分的访问模式。仅仅只有读模式3没有满足:谁关注了此用户。当前设计中,行键仅仅是用户ID,你需要浏览整个表数据来得到结果。这就让我们想到能不能在行键指出被关注的用户ID。

有两种方法来解决这个问题。第一种就是维护另外一张表包含反向列表(也就是此用户关注的用户列表)。第二种方法就是在一张表中使用不同的行键来存储关注和被关注的数据。上面的两种方法,都会实现信息的分隔,以至于你可以快速的访问。

对于当前表结构还可以进一步的优化,考虑下面的设计:

hbase表设计TTl hbase表设计 画像_hbase表设计TTl_08

有两件事需要指出:行键现在包括关注着和被关注者;列族的名称已经缩短为f。短的列族名称没有任何影响,它仅仅是为了提高IO操作。

这里获得一个被关注的列表变成了一个部分浏览,而不是之前的全表浏览。

取消关注和用户A是否关注用户B变成了简单的删除和get操作,而不是之前的遍历整行。这在关注者和别关注者列表很大的时候将变得很有用。

表中数据如下所示:

hbase表设计TTl hbase表设计 画像_数据模型_09

需要注意的是行键的长度是变化的。变化的长度使得监控性能变得困难,因为来自每个请求的长度不一致。

一个解决方法就是行键使用哈希值,为了得到长度一致的行键,你可以先哈希用户的id然后连接他们,而不是简单的连接在一起。

在查询的时候,因为你知道你要查询的用户ID,所以你可以重新计算哈希后,在进行查询。进行哈希后的表像下面这样:

hbase表设计TTl hbase表设计 画像_hbase表设计TTl_10

这个表设计可以有效的满足所有的访问模式。

总结

这篇文章包含了hbase的基础架构设计,开始介绍了数据模型,并介绍了在设计表结构是需要注意的事项。当然关于表设计还有很多需要去探索的地方。这篇文章的关键点如下:

  • 1.行键在表设计中非常重要,决定着应用中的交互以及提取数据的性能。
  • 2.hbase表示非常灵活的,你可以使用字节数组存储任何数据。
  • 3.存错任何数据到列族中,都可以使用相同的访问模式来访问数据。
  • 4.索引仅仅是行键,好好利用,将成为你的优势。
  • 5.深度高的表结构,可以使得你快速且简单的访问数据,但是却丢掉了原子性。宽度广的表结构,可以保证行级别的原子操作,但每行会有很多的列。
  • 6.你需要好好的思考你的表设计,使得可以使用单条API就可以操作,而不是使用多条。hbase不支持跨行的事务,也尽量避免在客户端代码中使用这样的逻辑。
  • 7.行键的哈希可以使得行键有固定的长度和更好的分布。但是却丢弃了使用字符串时的默认排序功能。
  • 8.列标识可以用来存储数据,就像单元数据一样。
  • 9.列标识的长度影响数据存储的足迹。也影响硬盘和网络IO的花销,所以应该尽量简洁。
  • 10.列族名字的长度影响到发送到客户端的数据长度。所以尽量简洁。