最近在学习HBase的使用,并仔细阅读了一篇官方推荐的博客,在这里就以一边翻译一边总结的方式和大家一起梳理一下HBase的数据模型和基本的表设计思路。
HBase是一个开源可伸缩的针对海量数据存储的分布式nosql数据库,它根据Google Bigtable数据模型来建模并构建在hadoop的hdfs存储系统之上。它和关系型数据库Mysql, Oracle等有明显的区别,HBase的数据模型牺牲了关系型数据库的一些特性但是却换来了极大的可伸缩性和对表结构的灵活操作。
时间戳(Timestamp): 默认下每一个单元中的数据插入时都会用时间戳来进行版本标识。读取单元数据时,如果时间戳没有被指定,则默认返回最新的数据,写入新的单元数据时,如果没有设置时间戳,默认使用当前时间。每一个列族的单元数据的版本数量都被HBase单独维护,默认情况下HBase保留3个版本数据。
../201807/~0,-4%.html
有时候,你也可以把HBase看成一个多维度的Map模型去理解它的数据模型。正如下图,一个行键映射一个列族数组,列族数组中的每个列族又映射一个列标识数组,列标识数组中的每一个列标识(Column Qualifier)又映射到一个时间戳数组,里面是不同时间戳映射下不同版本的值,但是默认取最近时间的值,所以可以看成是列标识(Column Qualifier)和它所对应的值的映射。用户也可以通过HBase的API去同时获取到多个版本的单元数据的值。Row Key在HBase中也就相当于关系型数据库的主键,并且Row Key在创建表的时候就已经设置好,用户无法指定某个列作为Row Key。
../201807/~0,-4%.html
又有的时候,你也可以把HBase看成是一个类似Redis那样的Key-Value数据库。如下图,当你要查询某一行的所有数据时,Row Key就相当于Key,而Value就是单元中的数据(列族,列族里的列和列中时间戳所对应的不同版本的值);当深入到HBase底层的存储机制时,用户要查询指定行里某一条单元数据时,HBase会去读取一个数据块,里面除了有要查询的单元数据,可能同时也会获取到其它单元数据,因为这个数据块还包含着这个Row Key所对应的其它列族或其它的列信息,这些信息实际也代表着另一个单元数据,这也是HBase的API内部实际的工作原理。
../201807/~0,-4%.html
第一种表结构设计中,在这种表结构设计中,每一行代表着某个用户和所有他所关注的其它用户。这个用户ID就是Row Key,而每一个列标识(Column Qualifier)就是这个用户所关注的其他用户在列族里的序号,单元数据就是这个用户所关注的其他用户的用户ID。在这种表结构的设计下,“每个用户都关注了谁”这个问题很好解决,但对于“用户A有没有关注用户B”这个问题在列很多的时候,需要遍历所有单元数据去找到用户B,这样的开销会十分大。并且当添加新的被关注用户时,因为不知道给这个新用户分配什么样的列族序号,需要遍历整个列族中的所有列找出最后一个列,并将最后一个列的序号+1给新的被关注用户作为列族内的序号,这样的开销也十分大。
../201807/~0,-4%.html
所以衍生出了第二种表结构设计,如下图,添加一个counter记录列族中所有列的总数量,当添加新的被关注用户时,这个新用户的序号就是counter+1。但是当要取消关注某个用户时,一样得遍历所有的列数据,而且最大的问题是在于HBase不支持事务处理,这种通过counter来添加被关注用户的操作逻辑得写在客户端中。
../201807/~0,-4%.html
回想一下,列标识(Column Qualifier)存储的时候是二进制的字节,所以列标识可以存储任何数据,而且列标识还是动态增添的,基于这个特性我们再改进表的设计,如下图。这次以被关注的用户ID做为列标识(Column Qualifier),然后单元数据可以是任意数字,比如全部统一成1。在这种表结构的设计下,添加新的被关注者,以及取消关注都会变得很简单。但是对于读场景中,谁关注了用户A这个问题,因为HBase数据库的索引只建立在Row Key上,这里不得不扫描全表去统计所有关注了用户A的用户数量,所以下面的这个表结构设计也存在一定的性能问题。这里也引出一个思路,被关注者需要以某种方式添加索引。
../201807/~0,-4%.html
针对上面的表结构有三种优化方案,第一种是新建另一张表,里面保存某个用户和所有关注他的用户。第二种解决方案就是在同一张表中也存储某个用户和所有关注他的用户的信息,并从Row Key中区分开来,比如:Row key为Jame_001_following的这行保存着所有Jame关注的人的信息,而Row_Key为Jame_001_followed的这行保存着所有关注Jame的人的信息。最后一种优化方案就是,如下图,将Row Key设计成“followerID+followedID”的形式,比如:“Jame+Emma”,这里的Row Key值就代表着Jame关注了Emma(其实这里应该是“Jame的ID+Emma的ID”,只是为了解释方便而直接用名字),同时包含了关注者和被关注者两个信息;还需要注意的一点就是列族的名字被设计成只有一个字母f,这样设计的好处就是减少了HBase对数据的I/O操作压力,同时减少了返回到客户端的数据字节,提高响应速度,因为每一个返回给客户端的KeyValue对象都会包含列族名字。同时将被关注人的用户名称也保存在了表中作为Column Qualifier,这样做的好处就是节省了去用户表查找用户名的资源。在这种表结构设计下,“用户A取消关注某个用户B”,“用户A有没有关注用户B?”的业务处理就会变得简单高效。
../201807/~0,-4%.html
还有一个需要注意的问题,就是在实际的生产环境中,还需要将Row Key使用MD5加密,一方面是使Row Key的长度都一致,能提高数据的存取性能。这方面的优化不在本文的讨论范围内。
../201807/~0,-4%.html