一致性哈希算法
在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。
一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。
3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。
4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
普通的哈希算法(也称硬哈希)采用简单取模的方式,将机器进行散列,这在cache环境不变的情况下能取得让人满意的结果,但是当cache环境动态变化时,这种静态取模的方式显然就不满足单调性的要求(当增加或减少一台机子时,几乎所有的存储内容都要被重新散列到别的缓冲区中)。
一致性哈希算法有多种具体的实现,包括Chord算法,KAD算法等实现,以上的算法的实现都比较复杂,这里介绍一种网上广为流传的一致性哈希算法的基本实现原理,感兴趣的同学可以根据上面的链接或者去网上查询更详细的资料。
一致性哈希算法的基本实现原理是将机器节点和key值都按照一样的hash算法映射到一个0~2^32的圆环上。当有一个写入缓存的请求到来时,计算Key值k对应的哈希值Hash(k),如果该值正好对应之前某个机器节点的Hash值,则直接写入该机器节点,如果没有对应的机器节点,则顺时针查找下一个节点,进行写入,如果超过2^32还没找到对应节点,则从0开始查找(因为是环状结构)。如图1所示

图 1
图1中Key K的哈希值在A与B之间,于是K就由节点B来处理。
另外具体机器映射时,还可以根据处理能力不同,将一个实体节点映射到多个虚拟节点。
经过一致性哈希算法散列之后,当有新的机器加入时,将只影响一台机器的存储情况,例如新加入的节点H的散列在B与C之间,则原先由C处理的一些数据可能将移至H处理,而其他所有节点的处理情况都将保持不变,因此表现出很好的单调性。而如果删除一台机器,例如删除C节点,此时原来由C处理的数据将移至D节点,而其它节点的处理情况仍然不变。而由于在机器节点散列和缓冲内容散列时都采用了同一种散列算法,因此也很好得降低了分散性和负载。而通过引入虚拟节点的方式,也大大提高了平衡性。

/* initialize conhash library 
 * @pfhash : hash function, NULL to use default MD5 method 
 * return a conhash_s instance 
 */ 
 CONHASH_API struct conhash_s* conhash_init(conhash_cb_hashfunc pfhash);/* finalize lib */ 
 CONHASH_API void conhash_fini(struct conhash_s *conhash);/* set node */ 
 CONHASH_API void conhash_set_node(struct node_s *node, 
 const char *iden, u_int replica);/* 
 * add a new node 
 * @node: the node to add 
 */ 
 CONHASH_API int conhash_add_node(struct conhash_s *conhash, 
 struct node_s *node);/* remove a node */ 
 CONHASH_API int conhash_del_node(struct conhash_s *conhash, 
 struct node_s *node); 
 …/* 
 * lookup a server which object belongs to 
 * @object: the input string which indicates an object 
 * return the server_s structure, do not modify the value, 
 * or it will cause a disaster 
 */ 
 CONHASH_API const struct node_s* 
 conhash_lookup(const struct conhash_s *conhash, 
 const char *object); 
 Libconhash is very easy to use. There is a sample in the project that shows how to use the library. 
 First, create a conhash instance. And then you can add or remove nodes of the instance, and look up objects. 
 The update node’s replica function is not implemented yet. 
 Hide Copy Code 
 /* init conhash instance */ 
 struct conhash_s *conhash = conhash_init(NULL); 
 if(conhash) 
 { 
 /* set nodes */ 
 conhash_set_node(&g_nodes[0], “titanic”, 32); 
 /* … *//* add nodes */
conhash_add_node(conhash, &g_nodes[0]);
/* ... */
printf("virtual nodes number %d\n", conhash_get_vnodes_num(conhash));
printf("the hashing results--------------------------------------:\n");

/* lookup object */
node = conhash_lookup(conhash, "James.km");
if(node) printf("[%16s] is in node: [%16s]\n", str, node->iden);
/* add nodes */
conhash_add_node(conhash, &g_nodes[0]);
/* ... */
printf("virtual nodes number %d\n", conhash_get_vnodes_num(conhash));
printf("the hashing results--------------------------------------:\n");

/* lookup object */
node = conhash_lookup(conhash, "James.km");
if(node) printf("[%16s] is in node: [%16s]\n", str, node->iden);

}
Reference
http://portal.acm.org/citation.cfm?id=258660
http://en.wikipedia.org/wiki/Consistent_hashing
http://www.spiteful.com/2008/03/17/programmers-toolbox-part-3-consistent-hashing/

分布式集群中的一致性hash
在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的hash(object)%N算法,那么在有机器添加或者删除后,很多原有的数据就无法找到了,这样严重的违反了单调性原则。接下来主要讲解一下一致性哈希算法是如何设计的:

环形Hash空间
按照常用的hash算法来将对应的key哈希到一个具有2^32次方个桶的空间中,即0~(2^32)-1的数字空间中。现在我们可以将这些数字头尾相连,想象成一个闭合的环形。如下图

把数据通过一定的hash算法处理后映射到环上
现在我们将object1、object2、object3、object4四个对象通过特定的Hash函数计算出对应的key值,然后散列到Hash环上。如下图:
Hash(object1) = key1;
Hash(object2) = key2;
Hash(object3) = key3;
Hash(object4) = key4;

将机器通过hash算法映射到环上
在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。
假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;

通过上图可以看出对象与机器处于同一哈希空间中,这样按顺时针转动object1存储到了NODE1中,object3存储到了NODE2中,object2、object4存储到了NODE3中。在这样的部署环境中,hash环是不会变更的,因此,通过算出对象的hash值就能快速的定位到对应的机器中,这样就能找到对象真正的存储位置了。

机器的删除与添加
普通hash求余算法最为不妥的地方就是在有机器的添加或者删除之后会照成大量的对象存储位置失效,这样就大大的不满足单调性了。下面来分析一下一致性哈希算法是如何处理的。
1. 节点(机器)的删除
以上面的分布为例,如果NODE2出现故障被删除了,那么按照顺时针迁移的方法,object3将会被迁移到NODE3中,这样仅仅是object3的映射位置发生了变化,其它的对象没有任何的改动。如下图:

  1. 节点(机器)的添加
    如果往集群中添加一个新的节点NODE4,通过对应的哈希算法得到KEY4,并映射到环中,如下图:
    通过按顺时针迁移的规则,那么object2被迁移到了NODE4中,其它对象还保持这原有的存储位置。通过对节点的添加和删除的分析,一致性哈希算法在保持了单调性的同时,还是数据的迁移达到了最小,这样的算法对分布式集群来说是非常合适的,避免了大量数据迁移,减小了服务器的的压力。

平衡性
根据上面的图解分析,一致性哈希算法满足了单调性和负载均衡的特性以及一般hash算法的分散性,但这还并不能当做其被广泛应用的原由,因为还缺少了平衡性。下面将分析一致性哈希算法是如何满足平衡性的。hash算法是不保证平衡的,如上面只部署了NODE1和NODE3的情况(NODE2被删除的图),object1存储到了NODE1中,而object2、object3、object4都存储到了NODE3中,这样就照成了非常不平衡的状态。在一致性哈希算法中,为了尽可能的满足平衡性,其引入了虚拟节点。
——“虚拟节点”( virtual node )是实际节点(机器)在 hash 空间的复制品( replica ),一实际个节点(机器)对应了若干个“虚拟节点”,这个对应个数也成为“复制个数”,“虚拟节点”在 hash 空间中以hash值排列。
以上面只部署了NODE1和NODE3的情况(NODE2被删除的图)为例,之前的对象在机器上的分布很不均衡,现在我们以2个副本(复制个数)为例,这样整个hash环中就存在了4个虚拟节点,最后对象映射的关系图如下:

根据上图可知对象的映射关系:object1->NODE1-1,object2->NODE1-2,object3->NODE3-2,object4->NODE3-1。通过虚拟节点的引入,对象的分布就比较均衡了。那么在实际操作中,正真的对象查询是如何工作的呢?对象从hash到虚拟节点到实际节点的转换如下图:

“虚拟节点”的hash计算可以采用对应节点的IP地址加数字后缀的方式。例如假设NODE1的IP地址为192.168.1.100。引入“虚拟节点”前,计算 cache A 的 hash 值:
Hash(“192.168.1.100”);
引入“虚拟节点”后,计算“虚拟节”点NODE1-1和NODE1-2的hash值:
Hash(“192.168.1.100#1”); // NODE1-1
Hash(“192.168.1.100#2”); // NODE1-2

参考:
[1] http://blog.huanghao.me/?p=14

一致性hash 分库分表
• 分库分表是目前解决单点数据库一种比较流行的做法,也相对成熟,但都有一个共同的问题,就是随着业务的增长,之前的分库分表容量不够了,需要扩容了,这时,使用一致性哈希或者分段哈希(静态哈希+配置规则)可以尽量的减少数据的迁移。这里只谈一谈一致性哈希的做法。
什么时候我们需要利用一致性哈希水平拆分数据库单表呢?
1、当我们拥有一个数据量非常大的单表,比如上亿条数据。
2、不仅数据量巨大,这个单表的访问读写也非常频繁,单机已经无法抗住 I/O 操作。
3、此表无事务性操作,如果涉及分布式事务是相当复杂的事情,在拆分此类表需要异常小心。
4、查询条件单一,对此表的查询更新条件常用的仅有1-2个字段,比如用户表中的用户id或用户名。
最后,这样的拆分也是会带来负面性的,当水平拆分了一个大表,不得不去修改应用程序或者开发db代理层中间件,这样会加大开发周期、难度和系统复杂性。
大众点评订单分库分表实践
转载:大众点评订单分库分表实践
背景
订单单表早已突破两百G,因查询维度较多,即使加了两个从库,各种索引优化,依然存在很多查询不理想的情况;加之去年大量的抢购活动的开展,数据库达到瓶颈,应用只能通过限速、异步队列等对其进行保护;同时业务需求层出不穷,原有的订单模型很难满足业务需求,但是基于原订单表的DDL又非常吃力,无法达到业务要求;随着这些问题越来越突出,订单数据库的切分就愈发急迫了。
我们的目标是未来十年内不需要担心订单容量的问题
垂直切分
先对订单库进行垂直切分,将原有的订单库分为基础订单库、订单流程库等,这篇文章就不展开讲了。

水平切分
垂直切分缓解了原来单集群的压力,但是在抢购时依然捉襟见肘,并且原有的的订单模型已经无法满足业务需求,于是我们设计了一套新的统一订单模型,为同时满足C端用户、B端商户、客服、运营等的需求,我们分别通过用户ID和商户ID进行切分,并通过PUMA同步到一个运营库

切分策略
1、查询切分
将id和库的mapping关系记录在一个单独的库中

优点:id和库的mapping算法可以随意更改
缺点:引入额外的单点
2、范围切分

比如按照时间区间或id区间来切分
优点:单表大小可控,天然水平扩展
缺点:无法解决集中写入瓶颈的问题
3、hash切分
一般采用mod来切分,下面着重讲一下mod的策略

数据水平切分后我们希望是一劳永逸或者是易于水平扩展的,所以推荐采用mod 2^n这种一致性哈希
以统一订单库为例,我们分库分表的方案是32*32的,即通过userId后四位mod 32分到32个库中,同时再将userId后四位div 32 mod 32将每个库分为32个表,共计分为1024张表。线上部署情况为8个集群(主从),每个集群4个库。
为什么说这种方式是易于水平扩展的呢?我们分析如下两个场景
场景一:数据库性能达到瓶颈
方法一:

按照现有规则不变,可以直接扩展到32个数据库集群
方法二:

如果32个集群也无法满足需求,那么将分库分表规则调整为(32*2^n)*(32/2^n),可以达到最多1024个集群
场景二:单表容量达到瓶颈(或者1024已经无法满足你)
方法:

假如单表都已突破200G,200*1024=200T(按照现有的订单模型算了算,大概一万千亿订单,相信这一天,恩,指日可待!),没关系,32*(32*2^n),这时分库规则不变,单库里的表再进行裂变,当然,在目前订单这种规则下(用userId后四位 mod)还是有极限的,因为只有四位,所以最多拆8192个表,至于为什么只取了后四位,后面会有篇幅讲到。
另外一个维度是通过shopId进行切分,规则8*8和userId比较类似,就不再赘述,需要注意的是shop库我们仅存储了订单主表,用来满足shop维度的查询。
唯一ID方案
这个方案也很多,主流的有那么几种
1、利用数据库自增ID
优点:最简单
缺点:单点风险、单机性能瓶颈
2、利用数据库集群并设置相应的步长(Flickr方案)
优点:高可用、ID较简洁
缺点:需要单独的数据库集群
3、Twitter snowflake
优点:高性能高可用、易拓展
缺点:需要独立的集群以及ZK
4、一大波GUID、Random算法
优点:简单
缺点:生成ID较长,有重复几率
我们的方案:
为了减少运营成本并减少额外的风险我们排除了所有需要独立集群的方案,采用了带有业务属性的方案:
时间戳+用户标识码+随机数
有下面几个好处:
方便、成本低
基本无重复的可能
自带分库规则,这里的用户标识码即为用户ID的后四位,在查询的场景下,只需要订单号就可以匹配到相应的库表而无需用户ID,只取四位是希望订单号尽可能的短一些,并且评估下来四位已经足够
可排序,因为时间戳在最前面
当然也有一些缺点,比如长度稍长,性能要比int/bigint的要稍差等。
其他问题?
事务支持:我们是将整个订单领域聚合体切分,维度一致,所以对聚合体的事务是支持的
复杂查询:垂直切分后,就跟join说拜拜了;水平切分后,查询的条件一定要在切分的维度内,比如查询具体某个用户下的各位订单等;禁止不带切分的维度的查询,即使中间件可以支持这种查询,可以在内存中组装,但是这种需求往往不应该在在线库查询或者可以通过其他方法转换到切分的维度以实现。
数据迁移
数据库拆分一般是业务发展到一定规模后的优化和重构,为了支持业务快速上线,很难一开始就分库分表,垂直拆分还好办,改改数据源就搞定了,一旦开始水平拆分,数据清洗就是个大问题,为此,我们经历了以下几个阶段
第一阶段:

数据库双写(事务成功以老模型为准),查询走老模型
每日job数据对账(通过DW),并将差异补平
通过job导历史数据
第二阶段:

历史数据导入完毕并且数据对账无误
依然是数据库双写,但是事务成功与否以新模型为准,在线查询切新模型
每日job数据对账,将差异补平
第三阶段:

老模型不再同步写入,仅当订单有终态时才会异步补上
此阶段只有离线数据依然依赖老的模型,并且下游的依赖非常多,待DW改造完就可以完全废除老模型了
一些思考:
并非所有表都需要水平拆分,要看增长的类型和速度,水平拆分是大招,拆分后会增加开发的复杂度,不到万不得已不使用在大规模并发的业务上,尽量做到在线查询和离线查询隔离,交易查询和运营/客服查询隔离,拆分的维度的选择很重要,要尽可能在解决拆分前的问题的基础上,便于开发数据库没你想象的那么坚强,需要保护,尽量使用简单的、良好索引的查询,这样数据库整体可控,也易于长期容量规划以及水平扩展。

作者:meng_philip123
链接:http://www.jianshu.com/p/e598a1bf0980
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

一致性hash在mysql水平拆分中的应用
Sharding(切片) 不是一门新技术,而是一个相对简朴的软件理念,就是当我们的数据库单机无法承受高强度的i/o时,我们就考虑利用 sharding 来把这种读写压力分散到各个主机上去。
所以Sharding 不是一个某个特定数据库软件附属的功能,而是在具体技术细节之上的抽象处理,是Horizontal Partitioning 水平扩展(或横向扩展)的解决方案,其主要目的是为突破单节点数据库服务器的 I/O 能力限制,注意这里是突破单点数据库服务器的“I/O”能力。
在MySql 5.1 中增加了对单表的 PARTITION(分区)支持,可以把一张很大的单表通过 partition 分区成很多物理文件,避免每次操作一个大文件,可以对读写新能有所提升,下面是一个 partition 分区的例子。
一张游戏的日志表,有几千万行的数据,记录了接近一年的游戏物品获取日志,如果不对它进行 partition 分区存储,每次统计和分析日志都会消耗大量的时间。然后我们新建一张分区表,把老的日志数据导入到新的数据,统计分析的时间就会节约很多。
CREATE TABLE xxxxxxxx (
crttm int(11) NOT NULL,
srvid int(11) NOT NULL,
evtid int(11) NOT NULL,
aid int(11) NOT NULL,
rid int(11) NOT NULL,
itmid int(11) NOT NULL,
itmnum int(11) NOT NULL,
gdtype int(11) NOT NULL,
gdnum int(11) NOT NULL,
islmt int(11) NOT NULL,
KEY crttm (crttm),
KEY itemid (itmid),
KEY srvid (srvid),
KEY gdtype (gdtype)
) ENGINE=myisam DEFAULT CHARSET=utf8
PARTITION BY RANGE (crttm)
(
PARTITION p201303 VALUES LESS THAN (unix_timestamp(‘2014-04-01’)),
PARTITION p201304 VALUES LESS THAN (unix_timestamp(‘2014-05-01’)),
PARTITION p201305 VALUES LESS THAN (unix_timestamp(‘2014-06-01’)),
PARTITION p201306 VALUES LESS THAN (unix_timestamp(‘2014-07-01’)),
PARTITION p201307 VALUES LESS THAN (unix_timestamp(‘2014-08-01’)),
PARTITION p201308 VALUES LESS THAN (unix_timestamp(‘2014-09-01’)),
PARTITION p201309 VALUES LESS THAN (unix_timestamp(‘2014-10-01’)),
PARTITION p201310 VALUES LESS THAN (unix_timestamp(‘2014-11-01’)),
PARTITION p201311 VALUES LESS THAN (unix_timestamp(‘2014-12-01’)),
PARTITION p201312 VALUES LESS THAN (unix_timestamp(‘2015-01-01’)),
PARTITION p201401 VALUES LESS THAN (unix_timestamp(‘2015-02-01’))
);
对于这种业务场景,使用 mysql 的 partition 就已经足够了,但是对于 i/o 非常频繁的大表,单机垂直升级也已经支撑不了,存储已经不是影响其性能的主要原因,这时候就要用到sharding了。
我们一般会将一张大表的唯一键作为 hash 的 key,比如我们想要水平拆分的是一张拥有3千万行数据的用户表,我们可以利用唯一的字段用户id作为拆分的依据,这样就可以依据如下的方式,将用户表水平拆分成3张,下面是伪代码,将老的用户数据导入到新的3个被水平拆分的数据库中。
if userId % 3 == 0:
#insert data in user_table (user_table_0 databaseip: 127.0.0.1)
elif userId % 3 == 1:
#insert data in user_table (user_table_1 databaseip: 127.0.0.2)
else:
#insert data in user_table (user_table_2 databaseip: 127.0.0.3)
我们还会对每一个被拆分的数据库,做一个双主 master 的副本集备份,至于backup,我们则可以使用 percona的cluster来解决。它是比 mysql m/s 或者 m/m 更靠谱的方案。 http://www.percona.com/software/percona-xtradb-cluster
所以最后拆分的拓扑图大致如下:
随着我们的业务增长,数据涨到5千万了,慢慢的发现3个sharding不能满足我们的需求了,因为服务器紧张,所以这时候BOSS打算再加2个sharding,以后会慢慢加到10个sharding。
所以我们得在之前的3台sharding服务器上分别执行导入数据代码,将数据根据新的hash规则导入到每台sharding服务器上。几乎5千万行数据每行都移动了一遍,如果服务器够牛逼,Mysql每秒的插入性能能高达 2000/s,即使这样整个操作,都要让服务暂停8个小时左右。这时候DBA的脸色已经不好看了,他应该是已经通宵在导数据了。
那有没有一种更好的办法,让添加或者删除 sharding 节点对整个分片系统的数据迁移量降低呢?
我们可以利用一致性哈希算法,把用户id散列到各个 sharding 节点,这样就可以保证添加和删除节点数据迁移影响较小。关于什么是一致性哈性算法,参考我的另一篇博客: http://snoopyxdy.blog.163.com/blog/static/601174402012722102446720/
这里介绍一个Node.js模块,hashring,github主页地址如下,上面有demo和api文档: https://github.com/3rd-Eden/node-hashring 这是一个使用的demo代码,我翻译了注释,供大家参考:
// 加载模块,返回HashRing的构造函数
var HashRing = require(‘hashring’);

//实例化HashRing,这个例子中,我们把各个服务器均匀的添加了,没有设置权重
// 设置了最大的缓冲区 10000
var ring = new HashRing([
‘127.0.0.1’,
‘127.0.0.2’,
‘127.0.0.3’,
‘127.0.0.4’
], ‘md5’, {
‘max cache size’: 10000
});

//我们获取这个字符串的服务器ip
var server = ring.get(‘foo bar banana’); // returns 127.0.0.x
console.log(server)

// 如果你想把数据冗余的存储在多个服务器上
ring.range(‘foo bar banana’, 2).forEach(function forEach(server) {
console.log(server); // do stuff with your server
});

// 对环上移除或新增加一台服务器
ring.add(‘127.0.0.7’).remove(‘127.0.0.1’);

var server = ring.get(‘foo bar banana’); // returns 127.0.0.x
console.log(server)
接下来我们就要验证这种方式的可行性。 第一,假如我们有3万条数据,根据一致性哈希算法存储好了之后,这个算法是否能够较平均的将3万条数据分散到3台sharding服务器上。 第二,当数据量增加到5万,然后我们增加2台sharding服务器后,这个算法移动的数据量和最终每台服务器上的数据分布是如何的。
connHashStep1.js将3万用户数据通过一致性哈希算法存储在3台服务器上

var HashRing = require(‘hashring’); 
 var ring = new HashRing([ 
 ‘127.0.0.1’, 
 ‘127.0.0.2’, 
 ‘127.0.0.3’, 
 ], ‘md5’, { 
 ‘max cache size’: 10000 
 });var record = { 
 ‘127.0.0.1’:0, 
 ‘127.0.0.2’:0, 
 ‘127.0.0.3’:0 
 }; 
 var userMap = {}for(var i=1; i<=30000; i++){ 
 var userIdStr = i.toString(); 
 var server = ring.get(userIdStr); 
 userMap[userIdStr] = server; 
 record[server]++; 
 }

console.log(record);
第一次利用一致性hash之后,每台服务器存储的用户数据。
{ ‘127.0.0.1’: 9162, ‘127.0.0.2’: 9824, ‘127.0.0.3’: 11014 }
connHashStep2.js将5万用户数据通过一致性哈希算法存储在3台服务器上,然后用户数据5万不改变,新增加2台sharding,查看新的5台sharding的用户数据存储情况以及计算移动的数据条数。
var HashRing = require(‘hashring’);
var ring = new HashRing([
‘127.0.0.1’,
‘127.0.0.2’,
‘127.0.0.3’,
], ‘md5’, {
‘max cache size’: 10000
});

var record = {
‘127.0.0.1’:0,
‘127.0.0.2’:0,
‘127.0.0.3’:0
};
var userMap = {}

for(var i=1; i<=50000; i++){
var userIdStr = i.toString();
var server = ring.get(userIdStr);
userMap[userIdStr] = server;
record[server]++;
}

console.log(record);

//新增加2个sharding节点
var record2 = {
‘127.0.0.1’:0,
‘127.0.0.2’:0,
‘127.0.0.3’:0,
‘127.0.0.4’:0,
‘127.0.0.5’:0,
};
ring.add(‘127.0.0.4’).add(‘127.0.0.5’)

var moveStep = 0;
for(var i=1; i<=50000; i++){
var userIdStr = i.toString();
var server = ring.get(userIdStr);
//当用户的存储server改变,则计算移动
if(userMap[userIdStr] && userMap[userIdStr] != server){
userMap[userIdStr] = server;
moveStep++;
}
record2[server]++;
}
console.log(record2);
console.log(‘move step:’+moveStep);
5万用户数据,存储在3台服务器上的数目:
{ ‘127.0.0.1’: 15238, ‘127.0.0.2’: 16448, ‘127.0.0.3’: 18314 }
当我们sharding增加到5台,存储在5台服务器上的数目:
{ ‘127.0.0.1’: 8869,
‘127.0.0.2’: 9972,
‘127.0.0.3’: 10326,
‘127.0.0.4’: 10064,
‘127.0.0.5’: 10769 }
最终我们移动的用户数量:
move step:20833
其实你会发现
20833 = 10064 + 10769
也就是说,我们只是将1-3节点的部分数据移动到了4,5节点,并没有多余的移动一行数据。根据上面的示例,如果是5千万数据,利用一致性哈希的算法,添加2个节点,仅需2-3小时就可以完成。
那么什么时候我们需要利用一致性哈希水平拆分数据库单表呢? 1、当我们拥有一个数据量非常大的单表,比如上亿条数据。 2、不仅数据量巨大,这个单表的访问读写也非常频繁,单机已经无法抗住 I/O 操作。 3、此表无事务性操作,如果涉及分布式事务是相当复杂的事情,在拆分此类表需要异常小心。 4、查询条件单一,对此表的查询更新条件常用的仅有1-2个字段,比如用户表中的用户id或用户名。 最后,这样的拆分也是会带来负面性的,当水平拆分了一个大表,不得不去修改应用程序或者开发db代理层中间件,这样会加大开发周期、难度和系统复杂性。