一个巧妙的分库分表设计:异构索引表

945 阅读4分钟

前言

最近计划参与一个换书活动,翻到《企业IT架构转型之道阿里巴巴中台战略思想与架构实战》这本书时,回想起令我印象比较深刻的一个知识点:“异构索引表”,所以在此记录并分享,和大家共同学习交流。

异构索引表的作用

如果《一致性哈希在分库分表的应用》说的是分库分表的方法和策略,那么本文所探讨的“异构索引表”,则是在实施分库分表过程中一个非常巧妙的设计,可以有效的解决分库分表的查询问题。

分库分表的查询问题

问题说明

在哈希分库分表时,为了避免分布不均匀造成的“数据倾斜”,通常会选择一些数据唯一的字段进行哈希操作,比如ID。

以订单表为例,通常有(id、uid、status、amount)等字段,通过id进行哈希取模运算分库分表之后,效果如下图

哈希分库分表效果

这样分库分表的方法没有问题,但是,在后期的开发和维护过程中,可能会存在潜在的问题。

举个例子:现在要查询uid为1的记录,应该去哪个表或库去查询?

对于用户来讲,这个场景可以说是非常频繁的。

这个时候就会发现,要想查询uid为1的记录,只能去所有的库或分表上进行查询,也就是所谓的“广播查询”。

整个查询过程大概是这样的

分库分表查询

性能问题

显然,整个查询过程需要进行全库扫描,涉及到多次的网络数据传输,一定会导致查询速度的降低和延迟的增加

数据聚合问题

另外,当这个用户有成千上万条数据时,不得已要在一个节点进行排序、分页、聚合等计算操作,需要消耗大量的计算资源和内存空间。对系统造成的负担也会影响查询性能。

这是一个非常典型的“事务边界大”的案例,即“一条SQL到所有的数据库去执行”。

那么如何解决这一痛点?

解决分库分表的查询问题

本文重点:“异构索引表”是可以解决这个问题的。

引入异构索引表

简单来说,“异构索引表”是一个拿空间换时间的设计。具体如下:

添加订单数据时,除了根据订单ID进行哈希取模运算将订单数据维护到对应的表中,还要对uid进行哈希取模运算,将uid和订单id维护在另一张表中,如图所示。

异构索引表

引入“异构索引表”后,因为同一个uid经过哈希取模运算后得到的结果是一致的,所以,该uid所有的订单id也一定会被分布到同一张user_order表中。

当查询uid为1的订单记录时,就可以有效地解决数据聚合存在的计算资源消耗全库扫描的低效问题了。

接下来,通过查询过程,看看这两个问题是怎么解决的。

引入后的查询过程

引入“异构索引表”后,查询uid为1的订单记录时,具体过程分为以下几步:

  1. 应用向中间件发送select * from order where uid = 1,请求查询uid为1的订单记录。
  2. 中间件根据uid路由到“异构索引表”:user_order,获得该uid相关的订单ID列表(排序、分页可以在此sql操作)。
  3. 中间件根据返回的订单ID,再次准确路由到对应的订单表:order
  4. 中间件将分散的订单数据进行聚合返回给应用。

引入异构索引表查询

看上去引入“异构索引表”之后,多了一个查询步骤,但换来的是:

  1. 根据订单ID准确路由到订单表,避免了全库扫描。
  2. user_order表进行了排序、分页等操作,避免大量数据回到中间件去计算。

异构索引表解决不了的场景

“异构索引表”只适合简单的分库分表查询场景,如果存在复杂的查询场景,还是需要借助搜索引擎来实现。

总结

异构索引表作为一种巧妙的设计,避免了分库分表查询存在的两个问题:全库扫描不必要的计算资源消耗

但是,异构索引表并不适用所有场景,对于复杂的查询场景可能需要结合其他技术或策略来解决问题。