【原创】浅谈分库分表

浅谈分库分表

 

       摘要:本文以实际项目为引子,从分库分表设计、数据库扩容、跨库跨表查询以及分页等方面简单的谈谈数据库分库分表的设计与运用。笔者认为没有通用完美的解决方案,所有的设计都应是与具体业务场景紧密联系的。

 

(一)数据分库与分表

 

首先抛出一个问题:当我们在谈数据库分库分表时,我们要解决的背后的问题是什么?

随着系统业务的发展,数据不断的飞速增长,在面对海量数查询的时候我们希望能够获得较为优秀的数据检索能力;同时在系统存储容量遇到瓶颈的时候我们也希望有一套简易的、可操作的DB扩容方案。那怎么才能做到这些呢?

单表数据量达到一定量级之后查询速度会越来越慢,即便是在走上索引的情况下也难以达到预想的效果;同样单机实例的存贮容量也是有额度限制。分库分表似乎成为无法逃避的现实。

不同系统在分库分表时差别很大:在某些系统中数据拆分的逻辑比较简单,Sharding key维度单一,或者仅仅分表就能很好的解决问题;但是,对于另一些系统来说数据拆分的逻辑相对复杂,可能无法通过单一的Key就能拆分出数据,往往需要多维度划分,并且需要分表以及分库设计。

一般情况下,数据库划分的基本思路可以归纳为:基于业务垂直划分;基于数据水平拆分;或者复杂一点的场景就需要将两者综合应用。

垂直划分

基于领域模型做数据的垂直切分是一种最佳实践。如将订单、产品、账户、财务等领域模型划分到不同的DB库里面。且由于各领域数据之间join展示场景较少,在这种情况下分库能获得很高的价值,同时各个系统之间的扩展性得到很大程度的提高。

水平切分

一般情况下, 对于绝大多数的应用系统在做到垂直分库之后,就可以很好的支撑业务系统的发展。但是对于一些大型的系统来说,垂直分库之后是远远不够的,还需要做数据的水平切分。

以上这些可以为基本的指导思想,在实际项目中应用时也会有所差别。在最终确定分库分表的规则之前,一个非常重要的要求就是:满足业务系统上的使用方式。

Sharding逻辑

分库分表框架的美学在于对sharding key的设计,其次依据规则解析定位数据源和目标schema就是非常容易的事情。

其核心思想流程,如下图所示:

 

 【原创】浅谈分库分表

 

对于分库分表规则的解析和实现,采用如下方案较为容易实施:

1)基于DAO层面实现:在DAO层实现不受ORM框架的制约、实现简单、易于根据系统特点进行灵活的定制、无需SQL解析,性能上表现会稍好一些,在定位多数据源的情况很有优势。

2)基于ORM框架实现:基于ORM框架的实现可充分利用框架提供的Sql拦截解析和路由能力,但是与具体的框架耦合紧密。

3)基于Spring的数据访问层实现:在DAOJDBC之间的Spring JdbcTemplate中嵌入Sharding Rule也是一个非常不错的选择,同时利用Spring动态数据源扩展功能,实现多数据源定位也很容易。

 

(二)分库分表设计与扩容

 

以下结合实际项目中的场景,讨论下分库分表设计及其扩容方案:

 

场景一:主数据模型非常稳定,基于单一主键查询无范围查询,系统数据的增长速度超出预期,单库、单表模式下已无法有效的支持系统业务的发展。

对于上述场景,一个比较优雅的解决方案,借鉴一致性哈希算法:

 

【原创】浅谈分库分表 

 

如上图所示,将数据均匀的分布在5张表中,一个简单的做法是:依据Sharding Key5取模,根据余数将数据散落到目标表中。

通常情况下这种方案能很好的工作。假设单表的容量规划是1000W的级别,如果我们分表的数量在100张表,那么通过该方案可以支持的数据量的为10亿级别,对一些大型系统来说此方案也具有一定可行性的。

但是,单机DB服务器是无法承受如此大的容量,为避免单点需要将其拆分到多台DB服务器之上。经过上面的讨论,我们的方案可以演化为下图所示:

 

 【原创】浅谈分库分表

  

如果不想后续扩容带来的数据迁移麻烦,可以考虑分足够数量的表以期望达到足够的数据余量。

基于上述分库分表方案还可以做一次水平扩容:即启用10DB Instance,每个Instance上部署一个Schema,总容量上有可以增加一倍的空间,但是必须付出一次数据迁移的代价。

 

场景二:主数据模型的字段可能变化剧烈;系统总体数据量将大幅增长;希望支持时间区间按月查询,并希望获得较高的查询性能;确定要求支持分页和排序功能。

首先我们回顾一下,基于上文的一致性Hash的分库分表策略。在面对情景二时,显然不是非常的合适。首先我们不可能分出非常多的表来,这对我们的查询性能是极大的挑战,这意味着我们要跨数不清的表来查询,那查询性能可想而知。

        因此,我们希望按照月份的维度来划分,拆出12张表来分别对应1月到12月不同月份的数据,这在满足按月份查询的场景中是一个比较理想的选择,而且数据按照时间维度均匀的分布在12张月份表中。其次系统还面临主数据模型字段剧烈增长的危险,这种情况字段拆分是不可避免的。

 如下图所示,字段拆分后每个数据段都对应起各自的12张表,各个数据段之间通过全局唯一建关联:

 

 【原创】浅谈分库分表

 

       上述方式很好的解决了字段扩张的需求,同时保证了在按月查询时有较好的查询性能。但是上述方案同样面临容量问题:按照单表1000W的容量规划,12张表所能支撑的最大容量仅为1.2亿。

这个容量对于一般应用系统来说足够用,然而对于那些面对海量数据的系统来说扩容是必须要面对的问题。扩容是一件非常麻烦的事情,能做到不扩容就不扩容,或者争取少量次数的扩容。

现在让我们回过头来仔细分析下的数据分布情况,假设数据从2012年开始入库落地,那么对于1月份和12月份的分表数据分布入下图所示:

 

 【原创】浅谈分库分表

 

       很显然,随着时间的推移单表数据量超过1000的容量规划是迟早的事情,那如何来做到线性的扩容呢,请看下图:

 

 【原创】浅谈分库分表

 

我们按照时间区段将数据分布在不同的DB实例之上,可以看出上述扩容方案以一种线性的方式进行,仅通过增加机器无需数据迁移,非常的简单。满足单表容量规划的要求,而且查询性能得到了保障,使得按时间区段做范围查询成为可能,很好的支持了业务的扩展。

其实要实现上述功能不难,在Sharding Layer层通过增加一张时间区段的范围映射表,每次查询或者写入的时候通过映射关系表定位目标数据源。

 

(三)分页查询性能优化

 

分页查询在单表的情况似乎是一件非常简单的事情,但是做了分库分表之后情况就变得非常的复杂,如果数据还要做JoinOrder By那么查询性能将急剧下降。

在分库分表的情况下,为了快速(分页)查询数据,分表策略的选择就显得非常重要了,需要尽最大限度将需要跨范围查询的数据尽量集中,多数情况下在我们做了最大限度的努力之后,数据仍然可能是分布式的。

为了进一步提高查询的性能,维持查询的中间变量信息是我们在分库分表模式下提高分页查询速度的另一个手段。

 

 【原创】浅谈分库分表

 

这样我们每次翻页查询时,通过中间信息的分析,就可以直接定位到目标表的目标位置,通过这种方式提供了近似于在单表模式下的分页查询能力。但另一方面也需要在业务上做出一定的牺牲:限制查询区段,提高检索速度。

笔者在项目中封装了一个基于场景二的分页查询组件, 对服务层调用透明,业务代码不需要关心分页查询的任何细节。其次在某些关键键值上添加设计分库分表信息,也使得在一些场景下的数据Join成为可能。

 

(四)小结

 

笔者认为没有通用完美的分库分表解决方案,本文给出的范例也只是结合自身项目需求的一些设计和考虑,并不一定是最合理的设计,但一定程度上满足了自身系统业务的需求。

 

如需转载,请注明出处。