海量数据分库分表方案(一)算法方案

本文主要描述分库分表的算法方案、按什么规则划分。按部就班比较目前出现的几种规则方式,最后第五种增量迁移方案是我设想和推荐的方式。后续章再讲述技术选型和分库分表后带来的问题。java

背景

随着业务量递增,数据量递增,一个表将会存下大量数据,在一个表有一千万行数据时,经过sql优化、提高机器性能还能承受。为了将来长远角度应在必定程度时进行分库分表,如出现数据库性能瓶颈、增长字段时须要耗时比较长的时间的状况下。解决独立节点承受全部数据的压力,分布多个节点,提供容错性,没必要一个挂整个系统不能访问。node

目的

本文讲述的分库分表的方案,是基于水平分割的状况下,选择不一样的规则,比较规则的优缺点。 通常网上就前三种,正常一点的会说第四种,但不是很完美,前面几种迁移数据都会很大影响,推荐我认为比较好的方案五。git

  • 方案一:对Key取模,除数逐步递增
  • 方案二:按时间划分
  • 方案三:按数值范围
  • 方案四:一致性Hash理念——平均分布方案(大众点评用这种,200G而且一步到位)
  • 方案五:一致性Hash理念——按迭代增长节点(为了方便增量迁移)
  • 方案六:一致性Hash理念——按范围分库(迭代迁移)

点赞再看,关注公众号:【地藏思惟】给你们分享互联网场景设计与架构设计方案 掘金:地藏Kelvin https://juejin.im/user/5d67da8d6fb9a06aff5e85f7算法

方案选择


方案一:对Key取模,除数逐步递增

公式:key mod x (x为天然数)sql

Key能够为主键,也能够为订单号,也能够为用户id,这个须要根据场景决定,哪一个做为查询条件几率多用哪一个。数据库

优势:服务器

  • 按需增长库、表,逐步增长
  • 分布均匀,每一片差别很少

缺点:架构

  • 不少时候会先从2开始分两个库逐级递增,而后分3个、4个、5个。如在mod 3 变 mod 5的状况下,取模后的大部分的数据的取模结果会变化,如key=3时,mod 3=0,忽然改变为mod 5=3,则将会从第0表迁移到第3表,将会形成不少数据多重复移动位置。
  • 会重复迁移数据,当分2个时,有数据A在第0号表,分3个时数据A去了第1号表、到分4个时数据A会回到第0号表

方案二:按时间划分

能够按日、按月、按季度。分布式

tb_20190101
tb_20190102
tb_20190103
……

这个算法要求在订单号、userId上添加年月日或者时间戳,或者查询接口带上年月日,才能定位在哪一个分片。函数

优势:

  • 数据按时间连续
  • 看数据增加比较直观

缺点:

  • 由于考虑到历史数据一开始没分库分表后续进行分库分表时,历史数据的订单号不必定有时间戳,历史数据可能为自增或者自定义算法得出的分布式主键,致使查询时必需要上游系统传订单号、建立时间两个字段。
  • 若上游系统没有传时间,或者上游系统的建立时间与当前系统对应订单的建立时间不在同一天的状况下,则当前数据库表的数据记录须要有时间字段。由于上游系统只传订单号,这个时候须要获取建立时间,当前系统就必需要有一个主表维护订单号和建立时间的关系,而且每次查询时都须要先查当前系统主表,再查具体表,这样就会消耗性能。
  • 分布不必定均匀:每个月增加数据不同,可能会有些月份多有些月份少

推荐使用场景:日志记录


方案三:按数值范围

表0 [0,10000000) 
表1 [10000000,20000000)
表2 [20000000,30000000)
表3 [30000000,40000000)
……

优势:

  • 分布均匀

缺点:

  • 由于未知最大值,因此没法用时间戳做为key,这个方法不能用表的自增主键,由于每一个表都自增数量不是统一维护。因此须要有一个发号器或发号系统作统一维护key自增的地方。

说后续推荐的方案中先简单说说一致性hash

先说一下一致性hash,有些文章说一致性Hash是一种算法,我认为它并非具体的计算公式,而是一个设定的思路。

1.先假定一个环形Hash空间,环上有固定最大值和最小值,头尾相连,造成一个闭环,如int,long的最大值和最小值。 不少文章会假定2^32个位置,最大值为2^32-1最小值为0,即0~(2^32)-1的数字空间,他们只是按照经常使用的hash算法举例,真实分库分表的状况下不是用这个数字,因此我才会认为一致性hash算法实际上是一个理念,并非真正的计算公式。 以下图

2. 设计一个公式函数 value = hash(key),这个公式将会有最大值和最小值,如 key mod 64 = value; 这个公式最大为64,最小为0。而后把数据都落在环上。

3. 设定节点node。设定节点的方式如对ip进行hash,或者自定义固定值(后续方案是使用固定值)。而后node逆时针走,直到前一个节点为止,途经value=hash(key)的全部数据的都归这个节点管。 如 hash(node1)=10,则hash(key)=0~10的数据都归node1管。

归纳

这里不详细说明这个理论,它主要表达的意思是固定好最大值,就再也不修改最大值到最小值范围,后续只修改节点node的位置和增长node来达到减小每一个node要管的数据,以达到减小压力。

备注:
* 不推荐对ip进行hash,由于可能会致使hash(ip)得出的结果很大,例如得出60,若这个节点的前面没有节点,则60号位置的这个节点须要管大部分的数据了。
* 最好生成key的方式用雪花算法snowFlake来作,至少要是不重复的数字,也不要用自增的形式。
* 推荐阅读铜板街的方案 订单号末尾添加user%64

方案四:一致性Hash理念——平均分布方案

利用一致性hash理论,分库选择hash(key)的公式基准为 value= key mod 64,分表公式value= key / 64 mod 64,key为订单号或者userId等这类常常查询的主要字段。(后续会对这个公式有变化) 咱们假定上述公式,则能够分64个库,每一个库64个表,假设一个表1千万行记录。则最大64 * 64 * 1000万数据,我相信不会有这一天的到来,因此咱们以这个做为最大值比较合理,甚至选择32 * 32均可以。

由于前期用不上这么多个表,一开始创建这么多表每一个表都insert数据,会形成浪费机器,因此在咱们已知最大值的状况下,咱们从小的数字开始使用,因此咱们将对上述计算得出的value进行分组。

分组公式:64 = 每组多少个count  * group须要分组的个数 
数据所在环的位置(也就是在哪一个库中):value = key mode 64 / count  * count

如下举例为16组,也就是16个库,group=16 这个时候库的公式为value = key mode 64 / 4 * 4,除以4后,会截取小数位得出一个整数,而后 * 4倍,就是数据所在位置。

// 按4个为一组,分两个表
count = 4:
Integer dbValue = userId % 64 / count * count ;

hash(key)在0~3之间在第0号库
hash(key)在4~7之间在第4号库
hash(key)在8~11之间在第8号库
……

备注:其实一开始能够64个为一组就是一个库,后续变化32个为一组就是两个库,从一个库到两个库,再到4个库,逐步递进。

从分1库开始扩容的迭代:

下图中举例分16组后,变为分到32组,须要每一个库都拿出一半的数据迁移到新数据,扩容直到分64个组。

能够看到当须要进行扩容一倍时须要迁移一半的数据量,以2^n递增,因此进行影响范围会比较大。

优势:

  • 若是直接拆分32组,那么就比较一劳永逸
  • 若是数据量比较大,未作过度表能够用一劳永逸方式。
  • 分布均匀
  • 迁移数据时不须要像方案一那样大部分的数据都须要进行迁移并有重复迁移,只须要迁移一半

缺点:

  • 能够扩展,可是影响范围大。
  • 迁移的数据量比较大,虽然不像方案一那样大部分数据迁移,当前方案每一个表或库都须要一半数据的迁移。
  • 若要一劳永逸,则须要总体停机来迁移数据

方案五:一致性Hash理念——按迭代增长节点

(我认为比较好的方案)

一致性hash方案结合比较范围方案,也就是方案三和方案四的结合。

解析方案四问题所在

方案四是设定最大范围64,按2^n指数形式从1增长库或者表数量,这样带来的是每次拆分进行迁移时会影响当整体数据量的1/2的数据,影响范围比较大,因此要么就直接拆分到32组、64组一劳永逸,要么每次1/2迁移。

方案四对应迁移方案:

  1. 第一种是停机迁移数据,成功后,再从新启动服务器。影响范围为全部用户,时间长。
  2. 第二种是把数据源切到从库,让用户只读,主库迁移数据,成功后再切到主库,虽然用户能适用,影响业务增量
  3. 第三种是设定数据源根据规则让一半的用户能只读,另外一半的用户能读能写,由于方案四迁移都是影响通常的数据的,因此最多能作到这个方式。

方案五详解

如今我想方法时,保持一致性hash理念,1个1个节点来增长,而不是方案四的每次增长2^n-n个节点。可是代码上就须要进行对新节点内的数据hash值判断。

咱们基于已经发生过1次迭代分了两个库的状况来作后续迭代演示,首先看看已经拆分两个库的状况:

数据落在第64号库名为db64和第32号库名为db32


迭代二: 区别与方案四直接增长两个节点,咱们只增长一个节点,这样迁移数据时由本来影响1/2的用户,将会只影响1/4的用户。

在代码中,咱们先把分组从32个一组改成16个一组,再给代码特殊处理 0~16的去到新的节点 16~32走回原来的32号节点 32~63走回原来64号节点 因此下面就要对节点特殊if else

// 按32改成16个为一组,分两变为4个库
count = 16;
Integer dbValue = userId  % 64 / count * count ;
if(dbValue<16){
    // 上一个迭代这些数据落在db32中,如今走新增节点名为db16号的那个库
    dbValue =  16;
    return dbValue;
} else {
    // 按原来规则走
    return dbValue;
}

迭代三:

这样就能够分迭代完成方案四种的一轮的迁移

迁移前能够先上线,增长一段开关代码,请求接口特殊处理hash值小于16的订单号或者用户号,这样就只会影响1/4的人

// 在请求接口中增长逻辑
    public void doSomeService(Integer userId){
        if(迁移是否完成的开关){
            // 若是未完成
            Integer dbValue = userId  % 64 / count * count ;
            if(dbValue<16){
                //这部分用户暂时不能走下面的逻辑
                return ;
            }
        }
        return dbValue;
    }
}
// 在分片时按32个为一组,分两个库
count = 16;
Integer dbValue = userId  % 64 / count * count ;
if(dbValue<16){
    // 上一个迭代这些数据落在db32中,有一半须要走新增节点名为db16号的那个库
    if(迁移是否完成的开关){
        // 若是已经完成,就去db16的库
        dbValue =  16;
    }
    return dbValue;
} else {
    // 按原来规则走
    return dbValue;
}

如此类推,下一轮总共8个节点时,每次迁移只须要迁移1/8。

其实也能够在第一个迭代时,不选择dbValue小于16号的来作。直接8个分一组,只选择dbValue<8的来作,这样第一个迭代的影响范围也会比较案例中小。上述案例用16只是比较好演示

优势:

  • 易于扩展
  • 数据逐渐增大过程当中,慢慢增长节点
  • 影响用户数量少
  • 按迭代进行,减小风险
  • 迁移时间短,如敏捷迭代思想

缺点:

  • 一段时间下不均匀

方案六:一致性Hash理念——按范围分库(迭代迁移)

如同上述方案五是方案四+方案一,能够达到逐步迁移数据,还有一种方案。就是方案四+方案三,只是不用取模后分组。

userId % 64 / count * count

由于上述公式,得出结果中,不必定每一片数据都是平均分布的。其实咱们能够取模后,按范围划分分片,以下公式。

第一片 0<userId  % 64<15
 第二片 16<userId  % 64<31
 第三片 32<userId  % 64<47
 第四片 48<userId  % 64<63

固然范围能够自定义,看取模后落入哪一个值的数量比较多,就切某一片数据就行了,具体就不画图了,跟方案四相似。

由于迁移数据的缘由,方案四中,若是数据量大,达到1000万行记录,每次迁移都须要迁移不少的数据,因此不少公司会尽早分库分表。

可是在业务优先状况下,一直迭代业务,数据一进达到不少的状况下16分支一也是不少的数据时,咱们就能够用一致性Hash理念--按范围分库


欢迎关注

个人公众号 :地藏思惟

掘金:地藏Kelvin

简书:地藏Kelvin

CSDN:地藏Kelvin

个人Gitee: 地藏Kelvin https://gitee.com/dizang-kelvin

相关文章
相关标签/搜索