分库分表做到永不迁移数据和避免热点


场景

项目中,如果数据量过大,就应该对数据进行拆分。有垂直与水平两种方式。

垂直拆分比较简单,相当于本来是一个数据库,数据量大了之后,从业务角度进行拆分。比如,独立拆分出订单库和用户库。

水平拆分,同一个业务数据量大了之后,进行水平拆分,比如,一张4000W的表,拆分成4张1000W的表

mysql单表存储量推荐是百万级,如果不进行处理,mysql单表数据太大,会导致性能变慢。使用方案可以参考数据进行水平拆分。把4000万数据拆分4张表或者更多。当然也可以分库,再分表;把压力从数据库层级分开。

分库分表方案

分库分表常用方案,hash取模和range范围;分库分表方案最主要就是路由算法,把路由的key按照指定的算法进行路由存放。

1、hash取模方案

在我们设计系统之前,可以先预估一下大概这几年的订单量,比如:4000W。每张表我们可以容纳1000W,可以设计4张表进行存储。

具体如何路由存储?hash的方案就是对指定的路由key(如:id)对分表总数进行取模,上图中,id=12的订单,对4进行取模,也就是会得到0,那么订单会放到0表中。id=13的订单,取模得到1,就会放到1表中。为什么对4取模,因为分表总数是4。

  • 优点: 订单数据可以均匀放到4张表中,这样对此订单进行操作时,就不会有热点问题

    热点含义:热点的意思就是对订单进行集中到1个表中,其他表的操作很少。 订单有个特点就是时间属性,一般用户操作订单数据,都会集中到这段时间产生的订单。如果这段时间产生的订单都在同一张表中,那就会形成热点,那张表的压力就会比较大。

  • 缺点 将来的数据迁移和扩容,会很难。 比如:业务发展好,订单量大,超出了4000W的量,我们就需要增加分表,如果我们增加4个表

一旦我们增加了分表的总数,取模基数就会变成8,以前id=12的订单按照此方案就会到4表中查询,但之前订单是在0表,这样就会导致数据查询不到。就是因为取模的基数产生了变化。

遇到这种情况,一般都是做数据迁移,把之前4000W的数据重新做hash方案,放到新规划的分表中。但是是个非常麻烦的事情,一般公司可以在晚上进行停机迁移,但是大公司是不允许做数据迁移。

做数据迁移可以结合自己公司的业务,做一个工具进行,不过很多工作量,每次扩容都要数据迁移

range范围方案

range方案就是以范围进行拆分数据

range的方案比较简单,就是把一定范围的订单存放到一个表中;如上图id=12放到0表,id=1300W放到1表。设计这个方案的前提就是把表的范围设计好。通过id进行路由存放。

  • 优点 此方案有利于将来的扩容,不需要做数据迁移。即时再增加4张表,之前的4张表范围不需要改表,id=12还是在0表中,id=1300W还是在1表中,新增的4张表的范围肯定大于4000W之后的范围。

  • 缺点 会有热点问题,因为id的值会一直增加,那么这段时间的订单不会一直在某一张表中,如果id在1000W~2000W之间这段时间产生的订单都会集中到这张表,导致1表压力过大,而其他的表没什么压力。

总结

hash取模方案:没有热点问题,但扩容迁移数据麻烦;

range方案:不需要迁移数据,但是会存在热点问题

那有什么方案可以做到两者的优点结合呢?,即不需要迁移数据,又能解决数据热点的问题呢?

方案思路

hash可以解决数据均匀的问题,range可以解决数据迁移的问题,那么我们可以两者结合,利用两者的特性。

数据扩容代表着,路由key(如id)的值变大,这个是一定的,那我们先保证数据变大的时候,先用range方案让数据落地到一个范围里面,这样以后id再变大,以前数据不用迁移。

此时又需要考虑到数据均匀,我们可以在一定范围内的数据均匀,因为我们每次扩容肯定会事先设计好这次扩容的范围大小,我们只要保证这次的范围内数据均匀就行了

设计方案

我们定义一个group组概念,这个组包含了一些分库与分表

上面的几个关键点

1)id=0~4000W肯定要落在group01中 2)group01组有3个DB,那一个id如何路由到哪个DB? 3)根据hash取模定位DB,模的基数是多少?模要为所有此group组DB中的表数量,上图总表数为10。为什么要取表的总数10?而不是DB总数3呢? 4)如id=12,id%10=2;那么值为2,落到哪个DB库呢?这是设计时前期设定好的,那怎么设定呢? 5)一旦设计定位哪个DB后,就需要确定落到DB中哪张表呢?

核心流程

按照上面的流程,我们可以根据此规则,定一个id,我们看看有没有避免热点问题。 id在【0,1000W】范围内的,根据上面的流程设计,1000W以内的id都均匀分配到DB_0,DB_1,DB_2三个数据库中的Table_0表中,为什么可以均匀,因为我们用了hash的方案,对10取模。

为什么对表总数10取模,而不是DB总数3取模?我们看一下DB_0为什么是4张表,其他两个DB_1与DB_2是3张表?

我们安排服务器有些服务器性能高,存储高,就可以安排多存放些数据,有些性能低就少放数据。如果我们取模按照DB总数3,那就代表【0,4000W】的数据平均分配到3个DB中,那就不能按照服务器能力适当分配了。

按照Table总数10就能达到,看如何达到:

上图中我们对10进行取模,如果值为【0,1,2,3】就路由到DB_0,【4,5,6】就到DB_1,【7,8,9】就到DB_2,这样就可以把多一点数据放到DB_0中,其他DB数据就可以少一点。

注意:不要被DB_1或DB_2中的table范围也是0~4000W疑惑,这个是范围区间,也就是id再哪些范围,落到哪个表而已

上面的操作,就解决了热点的问题,可以按照服务器的指标,设计数据量的分配

如何扩容

其实扩容直接再设计一个group02组,定义这个group的数据范围就ok了。

因为是心中的一个group组,所以就没有什么数据迁移的概念,完全就是新增的group组,而且这个group组防止了热点,也就是【4000W~5500W】都是均匀分到三个DB的table_0中,【5500万~7000万】数据均匀分配到table_1表中。

系统设计

把group,DB,table之间建立起关联关系

group与DB的关系

table和db的关系

我们开发的时候不需要每次都去查三张关联表,可以保存到缓存中,这样就不会影响性能。

一旦需要扩容,增加group02的关联关系,简单点的话,就凌晨配置,重启应用服务就行,但如果是大型公司,是不允许的,因为凌晨也有订单的。那怎么办呢?本地jvm缓存怎么更新呢?

其实方案也很多,可以使用zookeeper,也可以使用分布式配置,这里是比较推荐使用分布式配置中心的,可以将这些数据配置到分布式配置中心去。简单的方法是,本地直接刷新缓存。


参考




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • 2379. Minimum Recolors to Get K Consecutive Black Blocks
  • 2471. Minimum Number of Operations to Sort a Binary Tree by Level
  • 1387. Sort Integers by The Power Value
  • 2090. K Radius Subarray Averages
  • 2545. Sort the Students by Their Kth Score