mycat分布式mysql中间件(分片规则)

mycat 分片规则

     传统的分片策略都是基于单表,或者分片基于主键进行分配,或者某些场景下需要多个表依赖于一个分片,或者分片的字段并不是主键。

     a.  对于传统的数据库分片方式都是基于单个表格,对于表关联这种操作,则很难处理。为了能够执行t_usert_user_detail的联合查询, MyCAT借鉴了NewSQL领域的新秀Foundation DB的设计思路,Foundation DB创新性的提出了Table Group的概念,其将子表的存储位置依赖于主表,并且物理上紧邻存放,因此彻底解决了JOIN的效率和性能问题,根据这一思路,提出了基于E-R关系的数据分片策略,子表的记录与所关联的父表记录存放在同一个数据分片上。

t_usert_user_detail例子为例,schema.xml中定义如下的分片配置:

<table name="t_user" dataNode="dn$1-32" rule="mod-long">
    
<childTable name="t_user_detail" primaryKey="id" joinKey="user_id" parentKey="user_id" />
</table>

t_user采用mod-long这个分片策略,分片在dn1-dn32上,t_user_detail依赖父表进行分片,两个表的关联关系为t_user_detail.user_id=t_user.id。于是数据分片和存储的示意图如下:

这样一来,分片dn1-32上的t_userhn1-32上的t_user_detail就可以进行局部的JOIN联合,再合并两个节点的数据即可完成整体的JOIN,试想一下,每个分片上t_user_detail表有1000万条,则10个分片就有1个亿,基于E-R映射的数据分片模式,基本上解决了80%以上的企业应用所面临的问题。

 b. 

多对多的表格如何处理?多对多的表格通常情况下,有以下几种:

l  主表+关系表+字典表

l  主表A+关系表+主表B

对于第一种,字典表可以被定义为“全局表”,字典表的记录规模可以在几千到几十万之间,基本是变动比较少的表,由MyCAT自动实时同步到所有分片,这样就可以三个表都做JOIN操作了。

对于第二种,需要从业务角度来看,关系表更偏向哪个表,即“A的关系”还是“B的关系”,来决定关系表跟从那个方向存储。目前还暂时无法很好支持这种模式下的3个表之间的关联。未来版本中将考虑将中间表进行双向复制,以实现从A-关系表 以及B-关系表的双向关联查询。

关于全局表的实现方式,全局表在数据插入或更新的时候,会自动在全局表定义的所有数据节点上执行相同的操作,以保证所有数据节点都一致,由于这个特性,全局表可以跟任何分片或不分片的表格进行JOIN操作。对数据更新不频繁的,规模不是很大的(100万之内)的表都可以定义为MyCAT的全局表,以实现用存储换性能的目标。

配置为:


<table name="t_area" primaryKey="id" type="global" dataNode="dn1,dn2" />


c.  主键分片vs 非主键分片


当你没人任何字段可以作为分片字段的时候,主键分片就是唯一选择,其优点是按照主键的查询最快,当采用自动增长的序列号作为主键时,还能比较均匀的将数据分片在不同的节点上。

若有某个合适的业务字段比较合适作为分片字段,则建议采用此业务字段分片,选择分片字段的条件如下:

  • 尽可能的比较均匀分布数据到各个节点上;
  • 该业务字段是最频繁的或者最重要的查询条件。

常见的除了主键之外的其他可能分片字段有“订单创建时间”、“店铺类别”或“所在省”等。当你找到某个合适的业务字段作为分片字段以后,不必纠结于“牺牲了按主键查询记录的性能”,因为在这种情况下,MyCAT提供了“主键到分片”的内存缓存机制,热点数据按照主键查询,丝毫不损失性能。做法如下:

<table name="t_user" primaryKey="user_id" dataNode="dn$1-32" rule="mod-long">
<childTable name="t_user_detail" primaryKey="id" joinKey="user_id" parentKey="user_id" />
</table>

对于非主键分片的table,填写属性primaryKey,此时MyCAT会将你根据主键查询的SQL语句的第一次执行结果进行分析,确定该Table 的某个主键在什么分片上,并进行主键到分片ID的缓存。

     第二次或后续查询mycat会优先从缓存中查询是否有id-->node  即主键到分片的映射,如果有直接查询,通过此种方法提高了非主键分片的查询性能。

d.分片join


     不管是按照何种规则分片数据的join都是分布式系统难题,mycat提供了几种方式:

     1. 如果是全局表,分片内部的表相关与全局表join,分片内部会使用分片内部全局表join业务表,方式跨分片join

     2. E-R 关系的分片表,同样也只会发生分片内部join 即 父表join子表,也不会跨分片

     3. catlet  既不是全局不又不是E-R关系,mycat提供了人工智能分片join即,通过程序编程的方式,通过拦截sql语句,将多个表的join分拆成多个子select,

        然后再join,这种方式需要开发支持,编写java代码作为插件,此种方法的优点是无需修改应用代码,只需要拦截对应的sql做处理即可。

     4. 目前最新版mycat引入了分片join机制,即通过在查询时刻拉取join表的数据,同步到mycat本地,导入到NoSql数据库中,再做join order limit,

        目前此种方式为开发的最新方式,可以支持2个表夸分片join,无需特殊配置。

e. 主键生成方式

    主键的生成可以自己生成也可以由mycat提供方式解决,目前mycat提供了全局唯一主键的策略。分为文件方式,数据库方式两种。


全局序列号是MyCAT提供的一个新功能,为了实现分库分表情况下,表的主键是全局唯一,而默认的MySQL的自增长主键无法满足这个要求。全局序列号的语法符合               标准SQL规范,其格式为:

next value for MYCATSEQ_GLOBAL

其中MYCATSEQ_GLOBAL是序列号的名字,MyCAT自动创建新的序列号,免去了开发的复杂度,另外,MyCAT也提供了一个全局的序列号,名称                             为:MYCATSEQ_GLOBAL

注意,MYCATSEQ_必须大写才能正确识别。

MyCAT温馨提示:实践中,建议每个表用自己的序列号,序列号的命名建议为MYCATSEQ _tableName_ID_SEQ


MyCAT 1.3开始,支持自增长主键,依赖于全局序列号机制,建议采用数据库方式的全局序列号,并正确设置步长,以免影响实际性能。

         首先要开启数据库方式的全局序列号,对于需要定义自增长主键的表,建立对应的全局序列号,与table名称同名大写,如customer序列名为CUSTOMER,然后再 schema.xml 中对customer表的table元素增加属性autoIncrement值为true.

 <table name=”CUSTOMER”  autoIncrement=”true”>

执行insert into customer (name,company_id,sharding_id) values ('test',2,10000);查看效果, 暂不支持主键为null如:insert into customer (id,name,company_id,sharding_id) values (null,'test',2,10000);





Catlet 人工智能分片使用:

Mycat 1.3开始支持Java类编程方式实现复杂SQL处理,类似数据库的存储过程,Catlet是一个实现了Catlet接口的无状态Java,负责将编码实现某个SQL的处理过程,并返回响应报文给客户端,目前主要用于人工智能(非AI)编码实现跨分片SQL的处理逻辑,Demo中附带143行完成两个表JION的查询示例,采用流式处理机制,未来将会提供更多高质量API来简化跨分片复杂SQL的编程问题,下个版本有望实现不带子查询的两表关联查询自动处理,也采用此框架。


package org.opencloudb.sqlengine;
 
/**
 * mycat catlet ,used to execute sql and return result to client,some like
 * database's procedure.
 * must implemented as a stateless class and can process many SQL concurrently
 *
 * @author wuzhih
 *
 */
public interface Catlet {
 
    /*
     * execute sql in EngineCtx and return result to client
     */
    void processSQL(String sql, EngineCtx ctx);
 
Catlet 编写完成并编译通过以后,必须放在Mycat_home/catlets目录下,系统会动态加载相关class(需要按照Java Class的目录结构存放,比如com\hp\catlet\XXXCatlet.class,目前还不支持Jar文件)并每隔1分组扫描一次文件是否更新,若更新则自动重新加载,因此无需重启服务,下面的截图对应的是demo.catletes.MyHellowJion这个Catlet的目录结构和所有相关类的位置。
在Mysql命令行连接Mycat Server后,执行带Catlet注解的SQL,则启动具体的Catlet完成SQL的解析,如下面的例子,表明select a.*, b.title from travelrecord a ,hotnews b where a.id=b.id 这个SQL交给demo.catlets.MyHellowJoin来处理。
/*!mycat:catlet=demo.catlets.MyHellowJoin */select a.*, b.title from travelrecord a ,hotnews b where a.id=b.id
         想要运行上述Demo,可以将demo.catletes.MyHellowJion编译好,并将Class放入指定的目录,执行上述SQL。此外Demo源码存在于demo.catlets目录下,这部分是属于Mycat开发,具备Java开发能力并有这方面需求的同学,可以参考另一片文章《MyCAT人工智能解决跨分片SQL》了解详情。



常用的根据主键或非主键的分片规则配置:

1. 枚举法:

   通过在配置文件中配置可能的枚举id,自己配置分片,使用规则:


<tableRule name="sharding-by-intfile">
    <rule>
      <columns>user_id</columns>
      <algorithm>hash-int</algorithm>
    </rule>
  </tableRule>
<function name="hash-int" class="org.opencloudb.route.function.PartitionByFileMap">
    <property name="mapFile">partition-hash-int.txt</property>
    <property name="type">0</property>
    <property name="defaultNode">0</property>
  </function>

partition-hash-int.txt 配置:
10000=0
10010=1
DEFAULT_NODE=1


上面columns 标识将要分片的表字段,algorithm 分片函数,

其中分片函数配置中,mapFile标识配置文件名称,type默认值为0,0表示Integer,非零表示String,

所有的节点配置都是从0开始,及0代表节点1

/**
*  defaultNode 默认节点:小于0表示不设置默认节点,大于等于0表示设置默认节点

默认节点的作用:枚举分片时,如果碰到不识别的枚举值,就让它路由到默认节点
*                如果不配置默认节点(defaultNode值小于0表示不配置默认节点),碰到
*                不识别的枚举值就会报错,
*                like this:can't find datanode for sharding column:column_name val:ffffffff    
*/


2.固定分片hash算法


<tableRule name="rule1">
    <rule>
      <columns>user_id</columns>
      <algorithm>func1</algorithm>
    </rule>
</tableRule>

  <function name="func1" class="org.opencloudb.route.function.PartitionByLong">
    <property name="partitionCount">2,1</property>
    <property name="partitionLength">256,512</property>
  </function>


配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数,

partitionCount 分片个数列表,partitionLength 分片范围列表
分区长度:默认为最大2^n=1024 ,即最大支持1024分区

约束 :

count,length两个数组的长度必须是一致的。
1024 = sum((count[i]*length[i])). count和length两个向量的点积恒等于1024

用法例子:

        本例的分区策略:希望将数据水平分成3份,前两份各占25%,第三份占50%。(故本例非均匀分区)
        // |<---------------------1024------------------------>|
        // |<----256--->|<----256--->|<----------512---------->|
        // | partition0 | partition1 | partition2 |
        // | 共2份,故count[0]=2 | 共1份,故count[1]=1 |
        int[] count = new int[] { 2, 1 };
        int[] length = new int[] { 256, 512 };
        PartitionUtil pu = new PartitionUtil(count, length);

        // 下面代码演示分别以offerId字段或memberId字段根据上述分区策略拆分的分配结果
        int DEFAULT_STR_HEAD_LEN = 8; // cobar默认会配置为此值
        long offerId = 12345;
        String memberId = "qiushuo";

        // 若根据offerId分配,partNo1将等于0,即按照上述分区策略,offerId为12345时将会被分配到partition0中
        int partNo1 = pu.partition(offerId);

        // 若根据memberId分配,partNo2将等于2,即按照上述分区策略,memberId为qiushuo时将会被分到partition2中
        int partNo2 = pu.partition(memberId, 0, DEFAULT_STR_HEAD_LEN);


如果需要平均分配设置:平均分为4分片,partitionCount*partitionLength=1024

<function name="func1" class="org.opencloudb.route.function.PartitionByLong">
    <property name="partitionCount">4</property>
    <property name="partitionLength">256</property>
  </function>

3.范围约定

<tableRule name="auto-sharding-long">
    <rule>
      <columns>user_id</columns>
      <algorithm>rang-long</algorithm>
    </rule>
  </tableRule>
<function name="rang-long" class="org.opencloudb.route.function.AutoPartitionByLong">
    <property name="mapFile">autopartition-long.txt</property>
  </function>
# range start-end ,data node index
# K=1000,M=10000.
0-500M=0
500M-1000M=1
1000M-1500M=2
或
0-10000000=0
10000001-20000000=1


配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数,

rang-long 函数中mapFile代表配置文件路径

所有的节点配置都是从0开始,及0代表节点1,此配置非常简单,即预先制定可能的id范围到某个分片


4.求模法

<tableRule name="mod-long">
    <rule>
      <columns>user_id</columns>
      <algorithm>mod-long</algorithm>
    </rule>
  </tableRule>
  <function name="mod-long" class="org.opencloudb.route.function.PartitionByMod">
   <!-- how many data nodes  -->
    <property name="count">3</property>
  </function> 

配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数,

此种配置非常明确即根据id进行十进制求模预算,相比方式1,此种在批量插入时需要切换数据源,id不连续




5.日期列分区法

<tableRule name="sharding-by-date">
      <rule>
        <columns>create_time</columns>
        <algorithm>sharding-by-date</algorithm>
      </rule>
   </tableRule>  
<function name="sharding-by-date" class="org.opencloudb.route.function.PartitionByDate">
    <property name="dateFormat">yyyy-MM-dd</property>
    <property name="sBeginDate">2014-01-01</property>
    <property name="sPartionDay">10</property>
  </function>

配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数,

配置中配置了开始日期,分区天数,即默认从开始日期算起,分隔10天一个分区


Assert.assertEquals(true, 0 == partition.calculate("2014-01-01"));
Assert.assertEquals(true, 0 == partition.calculate("2014-01-10"));
Assert.assertEquals(true, 1 == partition.calculate("2014-01-11"));
Assert.assertEquals(true, 12 == partition.calculate("2014-05-01"));


6.通配取模


<tableRule name="sharding-by-pattern">
      <rule>
        <columns>user_id</columns>
        <algorithm>sharding-by-pattern</algorithm>
      </rule>
   </tableRule>
<function name="sharding-by-pattern" class="org.opencloudb.route.function.PartitionByPattern">
    <property name="patternValue">256</property>
    <property name="defaultNode">2</property>
    <property name="mapFile">partition-pattern.txt</property>

  </function>
partition-pattern.txt 
# id partition range start-end ,data node index
###### first host configuration
1-32=0
33-64=1
65-96=2
97-128=3
######## second host configuration
129-160=4
161-192=5
193-224=6
225-256=7
0-0=7


配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数,patternValue 即求模基数,defaoultNode 默认节点,如果配置了默认,则不会按照求模运算

mapFile 配置文件路径

配置文件中,1-32 即代表id%256后分布的范围,如果在1-32则在分区1,其他类推,如果id非数据,则会分配在defaoultNode 默认节点


String idVal = "0";
Assert.assertEquals(true, 7 == autoPartition.calculate(idVal));
idVal = "45a";
Assert.assertEquals(true, 2 == autoPartition.calculate(idVal));



7. ASCII码求模通配


<tableRule name="sharding-by-prefixpattern">
      <rule>
        <columns>user_id</columns>
        <algorithm>sharding-by-prefixpattern</algorithm>
      </rule>
   </tableRule>
<function name="sharding-by-prifixpattern" class="org.opencloudb.route.function.PartitionByPrefixPattern">
    <property name="patternValue">256</property>
    <property name="prefixLength">5</property>
    <property name="mapFile">partition-pattern.txt</property>

  </function>

partition-pattern.txt

# range start-end ,data node index
# ASCII
# 48-57=0-9
# 64銆�5-90=@銆丄-Z
# 97-122=a-z
###### first host configuration
1-4=0
5-8=1
9-12=2
13-16=3
###### second host configuration
17-20=4
21-24=5
25-28=6
29-32=7
0-0=7

配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数,patternValue 即求模基数,prefixLength ASCII 截取的位数

mapFile 配置文件路径

配置文件中,1-32 即代表id%256后分布的范围,如果在1-32则在分区1,其他类推 


此种方式类似方式6只不过采取的是将列种获取前prefixLength位列所有ASCII码的和进行求模sum%patternValue ,获取的值,在通配范围内的

即 分片数,

/**
* ASCII编码:
* 48-57=0-9阿拉伯数字
* 64、65-90=@、A-Z
* 97-122=a-z
*
*/

如 


String idVal="gf89f9a";
Assert.assertEquals(true, 0==autoPartition.calculate(idVal));

idVal="8df99a";
Assert.assertEquals(true, 4==autoPartition.calculate(idVal));

idVal="8dhdf99a";
Assert.assertEquals(true, 3==autoPartition.calculate(idVal));



8.编程指定


<tableRule name="sharding-by-substring">
      <rule>
        <columns>user_id</columns>
        <algorithm>sharding-by-substring</algorithm>
      </rule>
   </tableRule>
<function name="sharding-by-substring" class="org.opencloudb.route.function.PartitionDirectBySubString">
    <property name="startIndex">0</property> <!-- zero-based -->
    <property name="size">2</property>
    <property name="partitionCount">8</property>
    <property name="defaultPartition">0</property>
  </function>

配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数 

此方法为直接根据字符子串(必须是数字)计算分区号(由应用传递参数,显式指定分区号)。

例如id=05-100000002

在此配置中代表根据id中从startIndex=0,开始,截取siz=2位数字即05,05就是获取的分区,如果没传默认分配到defaultPartition




9.字符串hash解析

<tableRule name="sharding-by-stringhash">
      <rule>
        <columns>user_id</columns>
        <algorithm>sharding-by-stringhash</algorithm>
      </rule>
   </tableRule>
<function name="sharding-by-stringhash" class="org.opencloudb.route.function.PartitionByString">
    <property name="partitionLength">512</property> <!-- zero-based -->
    <property name="partitionCount">2</property>
    <property name="hashSlice">0:2</property>
  </function>

配置说明:

上面columns 标识将要分片的表字段,algorithm 分片函数 

函数中length代表字符串hash求模基数,count分区数,hashSlice hash预算位


即根据子字符串 hash运算


hashSlice : 0 means str.length(), -1 means str.length()-1


/**
     * "2" -&gt; (0,2)<br/>
     * "1:2" -&gt; (1,2)<br/>
     * "1:" -&gt; (1,0)<br/>
     * "-1:" -&gt; (-1,0)<br/>
     * ":-1" -&gt; (0,-1)<br/>
     * ":" -&gt; (0,0)<br/>
     */
例子:

String idVal=null;
 rule.setPartitionLength("512");
 rule.setPartitionCount("2");
 rule.init();
 rule.setHashSlice("0:2");
//		idVal = "0";
//		Assert.assertEquals(true, 0 == rule.calculate(idVal));
//		idVal = "45a";
//		Assert.assertEquals(true, 1 == rule.calculate(idVal));

 
 
 //last 4
 rule = new PartitionByString();
 rule.setPartitionLength("512");
 rule.setPartitionCount("2");
 rule.init();
 //last 4 characters
 rule.setHashSlice("-4:0");
 idVal = "aaaabbb0000";
 Assert.assertEquals(true, 0 == rule.calculate(idVal));
 idVal = "aaaabbb2359";
 Assert.assertEquals(true, 0 == rule.calculate(idVal));


10,一致性hash

<tableRule name="sharding-by-murmur">
      <rule>
        <columns>user_id</columns>
        <algorithm>murmur</algorithm>
      </rule>
   </tableRule>
<function name="murmur" class="org.opencloudb.route.function.PartitionByMurmurHash">
      <property name="seed">0</property><!-- 默认是0-->
      <property name="count">2</property><!-- 要分片的数据库节点数量,必须指定,否则没法分片-->
      <property name="virtualBucketTimes">160</property><!-- 一个实际的数据库节点被映射为这么多虚拟节点,默认是160倍,也就是虚拟节点数是物理节点数的160倍-->
      <!--
      <property name="weightMapFile">weightMapFile</property>
                     节点的权重,没有指定权重的节点默认是1。以properties文件的格式填写,以从0开始到count-1的整数值也就是节点索引为key,以节点权重值为值。所有权重值必须是正整数,否则以1代替 -->
      <!--
      <property name="bucketMapPath">/etc/mycat/bucketMapPath</property>
                      用于测试时观察各物理节点与虚拟节点的分布情况,如果指定了这个属性,会把虚拟节点的murmur hash值与物理节点的映射按行输出到这个文件,没有默认值,如果不指定,就不会输出任何东西 -->
  </function>
一致性hash预算有效解决了分布式数据的扩容问题,前1-9中id规则都多少存在数据扩容难题,而10规则解决了数据扩容难点


关于一致性hash详细:



一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简 单哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。 
    一致性hash算法提出了在动态变化的Cache环境中,判定哈希算法好坏的四个定义:
1、平衡性(Balance):平衡性是指哈希的结果能够尽可能分布到所有的缓冲中去,这样可以使得所有的缓冲空间都得到利用。很多哈希算法都能够满足这一条件。
2、单调性(Monotonicity):单调性是指如果已经有一些内容通过哈希分派到了相应的缓冲中,又有新的缓冲加入到系统中。哈希的结果应能够保证原有已分配的内容可以被映射到原有的或者新的缓冲中去,而不会被映射到旧的缓冲集合中的其他缓冲区。 
3、分散性(Spread):在分布式环境中,终端有可能看不到所有的缓冲,而是只能看到其中的一部分。当终端希望通过哈希过程将内容映射到缓冲上时,由于不同终端所见的缓冲范围有可能不同,从而导致哈希的结果不一致,最终的结果是相同的内容被不同的终端映射到不同的缓冲区中。这种情况显然是应该避免的,因为它导致相同内容被存储到不同缓冲中去,降低了系统存储的效率。分散性的定义就是上述情况发生的严重程度。好的哈希算法应能够尽量避免不一致的情况发生,也就是尽量降低分散性。 
4、负载(Load):负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同 的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。

    在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布式集群管理最基本的功能。如果采用常用的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的映射位置发生了变化,其它的对象没有任何的改动。如下图:
                                                              
2. 节点(机器)的添加 
    如果往集群中添加一个新的节点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. A piece of eritodiun unlike any other!