五分钟学会HBase二级索引的设计
一、原理
“二级多列索引”是针对目标记录的某个或某些列建立的“键-值”数据,以列的值为键,以记录的RowKey为值,当以这些列为条件进行查询时,引擎可以通过检索相应的“键-值”数据快速找到目标记录。由于HBase本身并没有索引机制,为了确保非侵入性,引擎将索引视为普通数据存放在数据表中,所以,如何解决索引与主数据的划分存储是引擎第一个需要处理的问题,为了能获得最佳的性能表现,我们并没有将主数据和索引分表储存,而是将它们存放在了同一张表里,通过给索引和主数据的RowKey添加特别设计的Hash前缀,实现了在Region切分时,索引能够跟随其主数据划归到同一Region上,即任意Region上的主数据其索引也必定驻留在同一Region上,这样我们就能把从索引抓取目标主数据的性能损失降低到最小。与此同时,特别设计的Hash前缀还在逻辑上把索引与主数据进行了自动的分离,当全体数据按RowKey排序时,排在前面的都是索引,我们称之为索引区,排在后面的均为主数据,我们称之为主数据区。最后,通过给索引和主数据分配不同的Column Family,又在物理存储上把它们隔离了起来。逻辑和物理上的双重隔离避免了将两类数据存放在同一张表里带来的副作用,防止了它们之间的相互干扰,降低了数据维护的复杂性,可以说这是在性能和可维护性上达到的最佳平衡。
二、设计demo
让我们通过一个示例来详细了解一下二级多列索引表的结构,假定有一张Sample表,使用四位数字构成Hash前缀[ii],范围从0000到9999,规划切分100个Region,则100个Region的RowKey区间分别为[0000,0099],[0100,0199],……,[9900,9999],以第一个Region为例,请看图1,所有数据按RowKey进行字典排序,自动分成了索引区和主数据区两段,主数据区的Column Family是d,下辖q1,q2,q3等Qualifier,为了简单起见,我们假定q1,q2,q3的值都是由两位数字组成的字符串,索引区的Column Family是i,它不含任何Qualifier,这是一个典型的“Dummy Column Family“,作为区别于d的另一个Column Family,它的作用就是让索引独立于主数据单独存储。接下来是最重要的部分,即索引和主数据的RowKey,我们先看主数据的RowKey,它由四位Hash前缀和原始ID两部分组成,其中Hash前缀是由引擎分配的一个范围在0000到9999之间的随机值,通过这个随机的Hash前缀可以让主数据均匀地散列到所有的Region上,我们看图1,因为Region 1的RowKey区间是[0000,0099],所以没有任何例外,凡是且必须是前缀从0000到0099的主数据都被分配到了Region 1上。
接下来看索引的RowKey,它的结构要相对复杂一些,格式为:RegionStartKey-索引名-索引键-索引值,与主数据不同,索引RowKey的前缀部分虽然也是由四位数字组成,但却不是随机分配的,而是固定为当前Region的StartKey,这是非常重要而巧妙的设计,一方面,这个值处在Region的RowKey区间之内,它确保了索引必定跟随其主数据被划分到同一个Region里;另一方面,这个值是RowKey区间内的最小值,这保证了在同一Region里所有索引会集中排在主数据之前。接下来的部分是“索引名”,这是引擎给每类索引添加的一个标识,用于区分不同类型的索引,图1中展示了两种索引:a和b,索引a是为字段q1和q2设计的两列联合索引,索引b是为字段q2和q3设计的两列联合索引,依次类推,我们可以根据需要设计任意多列的联合索引。再接下来就是索引的键和值了,索引键是由目标记录各对应字段的值组成,而索引值就是这条记录的RowKey。
现在,假定需要查询满足条件q1=01 and q2=02的Sample记录,分析查询字段和索引匹配情况可知应使用索引a,也就是说我们首先确定了索引名,于是在Region 1上进行scan的区间将从主数据全集收窄至[0000-a, 0000-b),接着拼接查询字段的值,我们得到了索引键:0102,scan区间又进一步收窄为[0000-a-0102, 0000-a-0103),于是我们可以很快地找到0000-a-0102-0000|63af51b2这条索引,进而得到了索引值,也就是目标数据的RowKey:0000|63af51b2,通过在Region内执行Get操作,最终得到了目标数据。需要特别说明的是这个Get操作是在本Region上执行的,这和通过HTable发出的Get有很大的不同,它专门用于获取Region的本地数据,其执行效率是非常高的,这也是为什么我们一定要将索引和它的主数据放在同一张表的同一个Region上的原因。
预分区示例
byte prefix =(byte) (Long.hashCode(timestamp) % <number of regionservers>);
byte[] rowkey =Bytes.add(Bytes.toBytes(prefix), Bytes.toBytes(timestamp);