关于数据库:你分库分表的姿势对么详谈水平分库分表

58次阅读

共计 10642 个字符,预计需要花费 27 分钟才能阅读完成。

一、背景

提起分库分表,对于大部分服务器开发来说,其实并不是一个陈腐的名词。随着业务的倒退,咱们表中的数据量会变的越来越大,字段也可能随着业务复杂度的升高而逐步增多,咱们为了解决单表的查问性能问题,个别会进行分表操作。

同时咱们业务的用户活跃度也会越来越高,并发量级一直加大,那么可能会达到单个数据库的解决能力下限。此时咱们为了解决数据库的解决性能瓶颈,个别会进行分库操作。不论是分库操作还是分表操作,咱们个别都有两种形式应答,一种是垂直拆分,一种是程度拆分。

对于两种拆分形式的区别和特点,互联网上参考资料泛滥,很多人都写过相干内容,这里就不再进行具体赘述,有趣味的读者能够自行检索。

此文次要具体聊一聊,咱们最实用最常见的程度分库分表形式中的一些非凡细节,心愿能帮忙大家防止走弯路,找到最合适本身业务的分库分表设计。

【注 1】本文中的案例均基于 Mysql 数据库,下文中的分库分表统指程度分库分表。

【注 2】后文中提到到 M 库 N 表,均指共 M 个数据库,每个数据库共 N 个分表,即总表个数其实为 M *N。

二、什么是一个好的分库分表计划?

2.1 计划可持续性

后期业务数据量级不大,流量较低的时候,咱们无需分库分表,也不倡议分库分表。然而一旦咱们要对业务进行分库分表设计时,就肯定要思考到分库分表计划的可持续性。

那何为可持续性?其实就是:业务数据量级和业务流量将来进一步升高达到新的量级的时候,咱们的分库分表计划能够继续应用。

一个艰深的案例,假设以后咱们分库分表的计划为 10 库 100 表,那么将来某个工夫点,若 10 个库依然无奈应答用户的流量压力,或者 10 个库的磁盘应用行将达到物理下限时,咱们的计划可能进行平滑扩容。

在后文中咱们将介绍下目前业界罕用的翻倍扩容法和一致性 Hash 扩容法。

2.2 数据偏斜问题

一个良好的分库分表计划,它的数据应该是须要比拟平均的扩散在各个库表中的。如果咱们进行一个拍脑袋式的分库分表设计,很容易会遇到以下相似问题:

a、某个数据库实例中,局部表的数据很多,而其余表中的数据却寥寥无几,业务上的体现常常是提早忽高忽低,飘忽不定。

b、数据库集群中,局部集群的磁盘应用增长特地块,而局部集群的磁盘增长却很迟缓。每个库的增长步调不统一,这种状况会给后续的扩容带来步调不统一,无奈对立操作的问题。

这边咱们定义分库分表最大数据偏斜率为:(数据量最大样本 – 数据量最小样本)/ 数据量最小样本。一般来说,如果咱们的最大数据偏斜率在 5% 以内是能够承受的。

三、常见的分库分表计划

3.1 Range 分库分表

顾名思义,该计划依据数据范畴划分数据的寄存地位。

举个最简略例子,咱们能够把订单表依照年份为单位,每年的数据寄存在独自的库(或者表)中。如下图所示:

/**
 * 通过年份分表
 *
 * @param orderId
 * @return
 */
public static String rangeShardByYear(String orderId) {int year = Integer.parseInt(orderId.substring(0, 4));
    return "t_order_" + year;
}

通过数据的范畴进行分库分表,该计划是最浮夸的一种分库计划,它也能够和其余分库分表计划灵便联合应用。时下十分风行的分布式数据库:TiDB 数据库,针对 TiKV 中数据的打散,也是基于 Range 的形式进行,将不同范畴内的 [StartKey,EndKey) 调配到不同的 Region 上。

上面咱们看看该计划的毛病:

  • a、最显著的就是数据热点问题,例如下面案例中的订单表,很显著以后年度所在的库表属于热点数据,须要承载大部分的 IO 和计算资源。
  • b、新库和新表的追加问题。个别咱们线上运行的应用程序是没有数据库的建库建表权限的,故咱们须要提前将新的库表提前建设,避免线上故障。

这点非常容易被忘记,尤其是稳固跑了几年没有迭代工作,或者人员又交替频繁的模块。

  • c、业务上的穿插范畴内数据的解决。举个例子,订单模块无奈防止一些中间状态的数据弥补逻辑,即须要通过定时工作到订单表中扫描那些长时间处于待领取确认等状态的订单。

这里就须要留神了,因为是通过年份进行分库分表,那么除夕的那一天,你的定时工作很有可能会漏掉上一年的最初一天的数据扫描。

3.2 Hash 分库分表

尽管分库分表的计划泛滥,然而 Hash 分库分表是最大众最广泛的计划,也是本文花最大篇幅形容的局部。

针对 Hash 分库分表的细节局部,相干的材料并不多。大部分都是论述一下概念举几个示例,而细节局部并没有特地多的深刻,如果未联合本身业务贸然参考援用,前期非常容易呈现各种问题。

在正式介绍这种分库分表形式之前,咱们先看几个常见的谬误案例。

常见谬误案例一:非互质关系导致的数据偏斜问题

public static ShardCfg shard(String userId) {int hash = userId.hashCode();
    // 对库数量取余后果为库序号
    int dbIdx = Math.abs(hash % DB_CNT);
    // 对表数量取余后果为表序号
    int tblIdx = Math.abs(hash % TBL_CNT);
 
    return new ShardCfg(dbIdx, tblIdx);
}

上述计划是首次使用者特地容易进入的误区,用 Hash 值别离对分库数和分表数取余,失去库序号和表序号。其实略微考虑一下,咱们就会发现,以 10 库 100 表为例,如果一个 Hash 值对 100 取余为 0,那么它对 10 取余也必然为 0。

这就意味着只有 0 库外面的 0 表才可能有数据,而其余库中的 0 表永远为空!

相似的咱们还能推导到,0 库外面的共 100 张表,只有 10 张表中 (个位数为 0 的表序号) 才可能有数据。这就带来了十分重大的数据偏斜问题,因为某些表中永远不可能有数据,最大数据偏斜率达到了无穷大。

那么很显著,该计划是一个未达到预期成果的谬误计划。数据的散落状况大抵示意图如下:

事实上,只有库数量和表数量非互质关系,都会呈现某些表中无数据的问题。

证实如下:

那么是不是只有库数量和表数量互质就可用用这种分库分表计划呢?比方我用 11 库 100 表的计划,是不是就正当了呢?

答案是否定的,咱们除了要思考数据偏斜的问题,还须要思考可持续性扩容的问题,个别这种 Hash 分库分表的计划前期的扩容形式都是通过翻倍扩容法,那 11 库翻倍后,和 100 又不再互质。

当然,如果分库数和分表数不仅互质,而且分表数为奇数(例如 10 库 101 表),则实践上能够应用该计划,然而我想大部分人可能都会感觉应用奇数的分表数比拟奇怪吧。

常见谬误案例二:扩容难以继续

如果避开了上述案例一的陷阱,那么咱们又很容易一头扎进另一个陷阱,大略思路如下;

咱们把 10 库 100 表看成总共 1000 个逻辑表,将求得的 Hash 值对 1000 取余,失去一个介于 [0,999) 中的数,而后再将这个数二次均分到每个库和每个表中,大略逻辑代码如下:

public static ShardCfg shard(String userId) {
        // ① 算 Hash
        int hash = userId.hashCode();
        // ② 总分片数
        int sumSlot = DB_CNT * TBL_CNT;
        // ③ 分片序号
        int slot = Math.abs(hash % sumSlot);
        // ④ 计算库序号和表序号的谬误案例
        int dbIdx = slot % DB_CNT ;
        int tblIdx = slot / DB_CNT ;
 
        return new ShardCfg(dbIdx, tblIdx);
    }

该计划的确很奇妙的解决了数据偏斜的问题,只有 Hash 值足够平均,那么实践上调配序号也会足够均匀,于是每个库和表中的数据量也能放弃较平衡的状态。

然而该计划有个比拟大的问题,那就是在计算表序号的时候,依赖了总库的数量,那么后续翻倍扩容法进行扩容时,会呈现扩容前后数据不在同一个表中,从而无奈施行。

如上图中,例如扩容前 Hash 为 1986 的数据应该寄存在 6 库 98 表,然而翻倍扩容成 20 库 100 表后,它调配到了 6 库 99 表,表序号产生了偏移。这样的话,咱们在后续在扩容的时候,不仅要基于库迁徙数据,还要基于表迁徙数据,十分麻烦且易错。

看完了下面的几种典型的谬误案例,那么咱们有哪些比拟正确的计划呢?上面将联合一些理论场景案例介绍几种 Hash 分库分表的计划。

罕用姿态一:规范的二次分片法

上述谬误案例二中,整体思路完全正确,只是最初计算库序号和表序号的时候,应用了库数量作为影响表序号的因子,导致扩容时表序号偏移而无奈进行。

事实上,咱们只须要换种写法,就能得出一个比拟大众化的分库分表计划。

public static ShardCfg shard2(String userId) {
        // ① 算 Hash
        int hash = userId.hashCode();
        // ② 总分片数
        int sumSlot = DB_CNT * TBL_CNT;
        // ③ 分片序号
        int slot = Math.abs(hash % sumSlot);
        // ④ 从新批改二次求值计划
        int dbIdx = slot / TBL_CNT ;
        int tblIdx = slot % TBL_CNT ;
 
        return new ShardCfg(dbIdx, tblIdx);
    }

大家能够留神到,和谬误案例二中的区别就是通过调配序号从新计算库序号和表序号的逻辑产生了变动。它的分配情况如下:

那为何应用这种计划就可能有很好的扩大持久性呢?咱们进行一个简短的证实:

通过下面论断咱们晓得,通过翻倍扩容后,咱们的表序号肯定维持不变,库序号可能还是在原来库,也可能平移到了新库中(原库序号加上原分库数),完全符合咱们须要的扩容持久性计划。

【计划毛病】

1、翻倍扩容法后期操作性高,然而后续如果分库数曾经是大几十的时候,每次扩容都十分消耗资源。

2、间断的分片键 Hash 值大概率会散落在雷同的库中,某些业务可能容易存在库热点(例如新生成的用户 Hash 相邻且递增,且新增用户又是高概率的沉闷用户,那么一段时间内生成的新用户都会集中在相邻的几个库中)。

罕用姿态二:关系表冗余

咱们能够将分片键对应库的关系通过关系表记录下来,咱们把这张关系表称为 ” 路由关系表 ”。

public static ShardCfg shard(String userId) {int tblIdx = Math.abs(userId.hashCode() % TBL_CNT);
        // 从缓存获取
        Integer dbIdx = loadFromCache(userId);
        if (null == dbIdx) {
            // 从路由表获取
            dbIdx = loadFromRouteTable(userId);
            if (null != dbIdx) {
                // 保留到缓存
                saveRouteCache(userId, dbIdx);
            }
        }
        if (null == dbIdx) {
            // 此处能够自在实现计算库的逻辑
            dbIdx = selectRandomDbIdx();
            saveToRouteTable(userId, dbIdx);
            saveRouteCache(userId, dbIdx);
        }
 
        return new ShardCfg(dbIdx, tblIdx);
    }

该计划还是通过惯例的 Hash 算法计算表序号,而计算库序号时,则从路由表读取数据。因为在每次数据查问时,都须要读取路由表,故咱们须要将分片键和库序号的对应关系记录同时保护在缓存中以晋升性能。

上述实例中 selectRandomDbIdx 办法作用为生成该分片键对应的存储库序号,这边能够非常灵活的动静配置。例如能够为每个库指定一个权重,权重大的被选中的概率更高,权重配置成 0 则能够将敞开某些库的调配。当发现数据存在偏斜时,也能够调整权重使得各个库的使用量调整趋势靠近。

该计划还有个长处,就是实践上后续进行扩容的时候,仅须要挂载上新的数据库节点,将权重配置成较大值即可,无需进行任何的数据迁徙即可实现。

如下图所示:最开始咱们为 4 个数据库调配了雷同的权重,实践上落在每个库的数据概率均等。然而因为用户也有高频低频之分,可能某些库的数据增长会比拟快。当挂载新的数据库节点后,咱们灵便的调整了每个库的新权重。

该计划仿佛解决了很多问题,那么它有没有什么不适宜的场景呢?当然有,该计划在很多场景下其实并不太适宜,以下举例说明。

a、每次读取数据须要拜访路由表,尽管应用了缓存,然而还是有肯定的性能损耗。

b、路由关系表的存储方面,有些场景并不适合。例如上述案例中用户 id 的规模大略是在 10 亿以内,咱们用单库百表存储该关系表即可。但如果例如要用文件 MD5 摘要值作为分片键,因为样本集过大,无奈为每个 md5 值都去指定关系(当然咱们也能够应用 md5 前 N 位来存储关系)。

c、饥饿占位问题,如下详叙

咱们晓得,该计划的特点是后续无需扩容,能够随时批改权重调整每个库的存储增长速度。然而这个愿景是比拟缥缈,并且很难施行的,咱们选取一个简略的业务场景思考以下几个问题。

【业务场景】:以用户寄存文件到云端的云盘业务为例,须要对用户的文件信息进行分库分表设计,有以下假设场景:

  • ①假设有 2 亿实践用户,假如以后有 3000W 无效用户。
  • ②均匀每个用户文件量级在 2000 个以内
  • ③用户 id 为随机 16 位字符串
  • ④初期为 10 库,每个库 100 张表。

咱们应用路由表记录每个用户所在的库序号信息。那么该计划会有以下问题:

第一:咱们总共有 2 亿个用户,只有 3000W 个产生过事务的用户。若程序不加解决,用户发动任何申请则创立路由表数据,会导致为大量理论没有事务数据的用户提前创立路由表。

笔者最后存储云盘用户数据的时候便遇到了这个问题,客户端 app 会在首页查问用户空间应用状况,这样导致简直一开始就为每个使用者调配好了路由。随着工夫的推移,这部分没有数据的 ” 静默 ” 的用户,随时可能开始他的云盘应用之旅而“复苏”,从而导致它所在的库迅速增长并超过单个库的空间容量极限,从而被迫拆分扩容。

解决这个问题的计划,其实就是只针对事务操作 (例如购买空间,上传数据,创立文件夹等等) 才进行路由的调配,这样对代码层面便有了一些倾入。

第二、依照后面形容的业务场景,一个用户最终均匀有 2000 条数据,假设每行大小为 1K,为了保障 B + 数的层级在 3 层,咱们限度每张表的数据量在 2000W,分表数为 100 的话,能够失去实践上每个库的用户数不能超过 100W 个用户。

也就是如果是 3000W 个产生过事务的用户,咱们须要为其调配 30 个库,这样会在业务后期,用户均匀数据量绝对较少的时候,存在十分大的数据库资源的节约。

解决第二个问题,咱们个别能够将很多数据库放在一个实例上,后续随着增长状况进行拆分。也能够后续针对将满的库,应用惯例伎俩进行拆分和迁徙。

罕用姿态三:基因法

还是由谬误案例一启发,咱们发现案例一不合理的次要起因,就是因为库序号和表序号的计算逻辑中,有公约数这个因子在影响库表的独立性。

那么咱们是否能够换一种思路呢?咱们应用绝对独立的 Hash 值来计算库序号和表序号。

public static ShardCfg shard(String userId) {int dbIdx = Math.abs(userId.substring(0, 4).hashCode() % DB_CNT);
    int tblIdx = Math.abs(userId.hashCode() % TBL_CNT);
    return new ShardCfg(dbIdx, tblIdx);
}

如上所示,咱们计算库序号的时候做了局部改变,咱们应用分片键的前四位作为 Hash 值来计算库序号。

这也是一种罕用的计划,咱们称为基因法,即应用原分片键中的某些基因(例如前四位)作为库的计算因子,而应用另外一些基因作为表的计算因子。该计划也是网上不少的实际计划或者是其变种,看起来十分奇妙的解决了问题,然而在理论生成过程中还是须要谨慎。

笔者曾在云盘的空间模块的分库分表实际中采纳了该计划,应用 16 库 100 表拆分数据,上线初期数据失常。然而当数据量级增长起来后,发现每个库的用户数量重大不均等,故猜想该计划存在肯定的数据偏斜。

为了验证观点,进行如下测试,随机 2 亿个用户 id(16 位的随机字符串),针对不同的 M 库 N 表计划,反复若干次后求平均值失去论断如下:

 8 库 100 表
min=248305(dbIdx=2, tblIdx=64), max=251419(dbIdx=7, tblIdx=8), rate= 1.25%            √
16 库 100 表
min=95560(dbIdx=8, tblIdx=42), max=154476(dbIdx=0, tblIdx=87), rate= 61.65%           ×
20 库 100 表
min=98351(dbIdx=14, tblIdx=78), max=101228(dbIdx=6, tblIdx=71), rate= 2.93%

咱们发现该计划中,分库数为 16,分表数为 100,数量最小行数仅为 10W 不到,然而最多的曾经达到了 15W+,最大数据偏斜率高达 61%。按这个趋势倒退上来,前期很可能呈现一台数据库容量曾经应用满,而另一台还剩下 30%+ 的容量。

该计划并不是肯定不行,而是咱们在采纳的时候,要综合分片键的样本规定,选取的分片键前缀位数,库数量,表数量,四个变量对最终的偏斜率都有影响。

例如上述例子中,如果不是 16 库 100 表,而是 8 库 100 表,或者 20 库 100 表,数据偏斜率都能升高到了 5% 以下的可承受范畴。所以该计划的暗藏的 ” 坑 ” 较多,咱们不仅要估算上线初期的偏斜率,还须要测算若干次翻倍扩容后的数据偏斜率。

例如你用着初期比拟完满的 8 库 100 表的计划,前期扩容成 16 库 100 表的时候,麻烦就接踵而至。

罕用姿态四:剔除公因数法

还是基于谬误案例一启发,在很多场景下咱们还是心愿相邻的 Hash 能分到不同的库中。就像 N 库单表的时候,咱们计算库序号个别间接用 Hash 值对库数量取余。

那么咱们是不是能够有方法去除掉公因数的影响呢?上面为一个能够思考的实现案例:

public static ShardCfg shard(String userId) {int dbIdx = Math.abs(userId.hashCode() % DB_CNT);
        // 计算表序号时先剔除掉公约数的影响
        int tblIdx = Math.abs((userId.hashCode() / TBL_CNT) % TBL_CNT);
        return new ShardCfg(dbIdx, tblIdx);
}

通过测算,该计划的最大数据偏斜度也比拟小,针对不少业务从 N 库 1 表降级到 N 库 M 表下,须要保护库序号不变的场景下能够思考。

罕用姿态五:一致性 Hash 法

一致性 Hash 算法也是一种比拟风行的集群数据分区算法,比方 RedisCluster 即是通过一致性 Hash 算法,应用 16384 个虚构槽节点进行每个分片数据的治理。对于一致性 Hash 的具体原理这边不再反复形容,读者能够自行翻阅材料。

这边具体介绍如何应用一致性 Hash 进行分库分表的设计。

咱们通常会将每个理论节点的配置长久化在一个配置项或者是数据库中,利用启动时或者是进行切换操作的时候会去加载配置。配置个别包含一个 [StartKey,Endkey) 的左闭右开区间和一个数据库节点信息,例如:

示例代码:

private TreeMap<Long, Integer> nodeTreeMap = new TreeMap<>();
 
@Override
public void afterPropertiesSet() {
    // 启动时加载分区配置
    List<HashCfg> cfgList = fetchCfgFromDb();
    for (HashCfg cfg : cfgList) {nodeTreeMap.put(cfg.endKey, cfg.nodeIdx);
    }
}
 
public ShardCfg shard(String userId) {int hash = userId.hashCode();
    int dbIdx = nodeTreeMap.tailMap((long) hash, false).firstEntry().getValue();
    int tblIdx = Math.abs(hash % 100);
    return new ShardCfg(dbIdx, tblIdx);
}

咱们能够看到,这种模式和上文形容的 Range 分表十分类似,Range 分库分表形式针对分片键自身划分范畴,而一致性 Hash 是针对分片键的 Hash 值进行范畴配置。

正规的一致性 Hash 算法会引入虚构节点,每个虚构节点会指向一个实在的物理节点。这样设计方案次要是可能在退出新节点后的时候,能够有计划保障每个节点迁徙的数据量级和迁徙后每个节点的压力放弃简直均等。

然而用在分库分表上,个别大部分都只用理论节点,引入虚构节点的案例不多,次要有以下起因:

a、应用程序须要破费额定的耗时和内存来加载虚构节点的配置信息。如果虚构节点较多,内存的占用也会有些不太乐观。

b、因为 mysql 有十分欠缺的主从复制计划,与其通过从各个虚构节点中筛选须要迁徙的范畴数据进行迁徙,不如通过从库降级形式解决后再删除冗余数据简略可控。

c、虚构节点次要解决的痛点是节点数据搬迁过程中各个节点的负载不平衡问题,通过虚构节点打散到各个节点中均摊压力进行解决。

而作为 OLTP 数据库,咱们很少须要忽然将某个数据库下线,新增节点后个别也不会从 0 开始从其余节点搬迁数据,而是前置筹备好大部分数据的形式,故一般来说没有必要引入虚构节点来减少复杂度。

四、常见扩容计划

4.1 翻倍扩容法

翻倍扩容法的次要思维是每次扩容,库的数量均翻倍解决,而翻倍的数据源通常是由原数据源通过主从复制形式失去的从库升级成主库提供服务的形式。故有些文档将其称作 ”从库降级法“。

实践上,通过翻倍扩容法后,咱们会多一倍的数据库用来存储数据和应答流量,原先数据库的磁盘使用量也将失去一半空间的开释。如下图所示:

具体的流程大抵如下:

①、工夫点 t1:为每个节点都新增从库,开启主从同步进行数据同步。

②、工夫点 t2:主从同步实现后,对主库进行禁写。

此处禁写次要是为了保证数据的正确性。若不进行禁写操作,在以下两个工夫窗口期内将呈现数据不统一的问题:

a、断开主从后,若主库不禁写,主库若还有数据写入,这部分数据将无奈同步到从库中。

b、利用集群辨认到分库数翻倍的工夫点无奈严格统一,在某个工夫点可能两台利用应用不同的分库数,运算到不同的库序号,导致谬误写入。

③、工夫点 t3:同步齐全实现后,断开主从关系,实践上此时从库和主库有着齐全一样的数据集。

④、工夫点 t4:从库降级为集群节点,业务利用辨认到新的分库数后,将利用新的路由算法。

个别状况下,咱们将分库数的配置放到配置核心中,当上述三个步骤实现后,咱们批改分库数进行翻倍,利用失效后,应用服务将应用新的配置。这里须要留神的是,业务利用接管到新的配置的工夫点不肯定统一,所以必然存在一个工夫窗口期,该期间局部机器应用原分库数,局部节点应用新分库数。这也正是咱们的禁写操作肯定要在此步实现后能力放开的起因。

⑤、工夫点 t5:确定所有的利用均承受到库总数的配置后,放开原主库的禁写操作,此时利用完全恢复服务。

⑥、启动离线的定时工作,革除各库中的约一半冗余数据。

为了节俭磁盘的使用率,咱们能够抉择离线定时工作革除冗余的数据。也能够在业务初期表结构设计的时候,将索引键的 Hash 值存为一个字段。那么以上述罕用姿态四为例,咱们离线的革除工作能够简略的通过 sql 即可实现(须要避免锁住全表,能够拆分成若干个 id 范畴的子 sql 执行):

delete from db0.tbl0 where hash_val mod 4 <> 0;

delete from db1.tbl0 where hash_val mod 4 <> 1;

delete from db2.tbl0 where hash_val mod 4 <> 2;

delete from db3.tbl0 where hash_val mod 4 <> 3;

具体的扩容步骤可参考下图:

总结:通过上述迁徙计划能够看出,从工夫点 t2 到 t5 工夫窗口呢内,须要对数据库禁写,相当于是该工夫范畴内服务器是局部有损的,该阶段整体耗时差不多是在分钟级范畴内。若业务能够承受,能够在业务低峰期进行该操作。

当然也会有不少利用无奈容忍分钟级写入不可用,例如写操作远远大于读操作的利用,此时能够联合 canel 开源框架进行窗口期内数据双写操作以保证数据的一致性。

该计划次要借助于 mysql 弱小欠缺的主从同步机制,能在事先提前准备好新的节点中大部分须要的数据,节俭大量的人为数据迁徙操作。

然而毛病也很显著,一是过程中整个服务可能须要以有损为代价,二是每次扩容均须要对库数量进行翻倍,会提前节约不少的数据库资源。

4.2 一致性 Hash 扩容

咱们次要还是看下不带虚构槽的一致性 Hash 扩容办法,如果以后数据库节点 DB0 负载或磁盘应用过大须要扩容,咱们通过扩容能够达到例如下图的成果。

下图中,扩容前配置了三个 Hash 分段,发现[-Inf,-10000)范畴内的的数据量过大或者压力过高时,须要对其进行扩容。

次要步骤如下:

①、工夫点 t1:针对须要扩容的数据库节点减少从节点,开启主从同步进行数据同步。

②、工夫点 t2:实现主从同步后,对原主库进行禁写。

此处起因和翻倍扩容法相似,须要保障新的从库和原来主库中数据的一致性。

③、工夫点 t3:同步齐全实现后,断开主从关系,实践上此时从库和主库有着齐全一样的数据集。

④、工夫点 t4:批改一致性 Hash 范畴的配置,并使应用服务从新读取并失效。

⑤、工夫点 t5:确定所有的利用均承受到新的一致性 Hash 范畴配置后,放开原主库的禁写操作,此时利用完全恢复服务。

⑥、启动离线的定时工作,革除冗余数据。

能够看到,该计划和翻倍扩容法的计划比拟相似,然而它更加灵便,能够依据以后集群每个节点的压力状况选择性扩容,而无需整个集群同时翻倍进行扩容。

五、小结

本文次要形容了咱们进行程度分库分表设计时的一些常见计划。

咱们在进行分库分表设计时,能够抉择例如范畴分表,Hash 分表,路由表,或者一致性 Hash 分表等各种计划。进行抉择时须要充分考虑到后续的扩容可持续性,最大数据偏斜率等因素。

文中也列举了一些常见的谬误示例,例如库表计算逻辑中公约数的影响,应用前若干位计算库序号常见的数据歪斜因素等等。

咱们在理论进行抉择时,肯定要思考本身的业务特点,充沛验证分片键在各个参数因子下的数据偏斜水平,并提前布局思考好后续扩容的计划。

作者:vivo 平台产品开发团队 -Han Lei

正文完
 0