海量数据分库分表方案(一)算法方案
本文主要描述分库分表的算法方案、按什么规则划分。循序渐进比较目前出现的几种规则方式,最后第五种增量迁移方案是我设想和推荐的方式。后续章再讲述技术选型和分库分表后带来的问题。
背景
随着业务量递增,数据量递增,一个表将会存下大量数据,在一个表有一千万行数据时,通过sql优化、提升机器性能还能承受。为了未来长远角度应在一定程度时进行分库分表,如出现数据库性能瓶颈、增加字段时需要耗时比较长的时间的情况下。解决独立节点承受所有数据的压力,分布多个节点,提供容错性,不必一个挂整个系统不能访问。
目的
本文讲述的分库分表的方案,是基于水平分割的情况下,选择不同的规则,比较规则的优缺点。
一般网上就前三种,正常一点的会说第四种,但不是很完美,前面几种迁移数据都会很大影响,推荐我认为比较好的方案五。
- 方案一:对Key取模,除数逐步递增
- 方案二:按时间划分
- 方案三:按数值范围
- 方案四:一致性Hash理念——平均分布方案(大众点评用这种,200G并且一步到位)
- 方案五:一致性Hash理念——按迭代增加节点(为了方便增量迁移)
- 方案六:一致性Hash理念——按范围分库(迭代迁移)
点赞再看,关注公众号:【地藏思维】给大家分享互联网场景设计与架构设计方案
掘金:地藏Kelvin https://juejin.im/user/5d67da8d6fb9a06aff5e85f7
方案选择
方案一:对Key取模,除数逐步递增
公式:key mod x (x为自然数)
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的最大值和最小值。
很多文章会假定232个位置,最大值为232-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迁移。
方案四对应迁移方案:
- 第一种是停机迁移数据,成功后,再重新启动服务器。影响范围为所有用户,时间长。
- 第二种是把数据源切到从库,让用户只读,主库迁移数据,成功后再切到主库,虽然用户能适用,影响业务增量
- 第三种是设定数据源根据规则让一半的用户能只读,另一半的用户能读能写,因为方案四迁移都是影响一般的数据的,所以最多能做到这个方式。
方案五详解
现在我想方法时,保持一致性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