导航

一致性哈希的个人理解——从工作中的分表业务开始说起

Posted on 2018-05-30 23:13  zouyq  阅读(335)  评论(1)    收藏  举报
  • 一致性哈希算法到底是用来干什么的,它能解决业务中的哪些痛点,哪里需要这种算?相信大家都是带着疑问来的吧,先不急,咱们先从工作中常见的分表业务说起:
  1. 从分表说起
    • 日渐膨胀的order表
        拿工作中相关的支付来说吧,购买支付相关的order表,随着用户规模的日益扩大,购买业务的日益增长订单表会日益增大,当数据量增加到千万甚至上亿行级别时,即使走了索引,查询效率依旧很低,就需要对order表进行分表了
    • 分表维度
        我们先假定order表分10张表,此时我们需要考虑将原有order表中一张表的数据分配到order_part_x表中,需要考虑两个方面的问题:1.分表后,数据尽可能均匀分配;2.以前主表的查询可以很轻松迁移到分表中;考虑到order表查询都是通过用户user_id来查询的,可以考虑将原order表中user_id和分表数直接取余,当然,为了尽可能散列,可以将user_id取hash值,再和分表数进行取余操作,即:hash(user_id)%size,以此来确定数据会落入哪一个分表中,查询订单的时候同理,通过此方式可以先获取到特定用户的订单会落入到哪一个分表中,确定了order_part_x,那么查询就和之前单表查询一样了。  
  2. cache的数据分配
    假设业务需要,现有N台cache服务器,我们需要将key-value类型的数据均匀分配到这N台cache服务器中,以此减轻数据库的压力。还是类似上文分表的思路,通过计算hash(key)%size,以余数来确定该数据该存放到哪一台cache,这样通过key,就可以确定数据该到或者到了哪台机器上了。这么做看似没什么问题,但是考虑到以下几种特殊情况:当其中一台cache服务器宕机了,需要将其中一台缓存服务器拿掉,那么对应的,数据到server的映射关系就变成了hash(key)%(size-1),当数据需求继续增长,需要更多的缓存服务器,我们将cache server增加1台,那么之前数据到server的映射关系就变成了hash(key)%(size+1),这种变动直接会导致之前cache中存放的数据几乎全部失效,因缓存无法命中,所有查询请求将会直接冲击数据库服务器,导致严重后果。显然,这种哈希取余的算法已经完全不适合做cache的分配了。
  3. hash一致算法
    既然如此,有没有更好的办法,既解决了数据到缓存server的分配,又能灵活应对cache server的增减呢?就来到了刚所说的hash一致算法来了(Consistent Hashing),用自己的话简单来说,就是节点不再是具体的一个值了,而是一个范围区间,数据取哈希值落在对应节点的范围中间,就可以确定落在哪个节点上了
    • 节点和数据
    1. 将节点node通过取hash值,映射到 [0, 2^32 - 1]范围之内,映射的规则可为 IP、hostname 等。
      可以将其想象为一个圆,映射的节点Node_n和下一个节点占据了圆的一个范围[a,b),如下图所示:

       

    2. 将数据object映射到数值空间 [0, 2^32 - 1]
      规则可以简要描述为:对于所有满足 hash(node) <= hash(object) 的节点,选择 hash(node) 最大的节点存放 object。如果没有满足上述条件的节点,选择 hash(node) 最小的节点存放该 object,如下图所示:

       



    3. 当某个节点宕机时:
      假设上图中的node 2节点挂了,我们只需要将node 2节点的数据迁移到node 3来,其他节点没任何影响

       



    4. 新增一个节点:
      假设Node 2和Node 3直接新增一个Node 2_1节点,只需要将原有Node 2的一部分数据迁移到Node 2_1上面去,其他节点也没任何影响

       

    5. 下面,采用java语言来实现上述算法,看代码:
      package com.zyq;
      
      import java.security.MessageDigest;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.SortedMap;
      import java.util.TreeMap;
      
      /**
       * Created with IntelliJ IDEA.
       * User: zouyiq@gmail.com
       * Date: 2018/1/4
       * Time: 22:55
       * To change this template use File | Settings | File Templates.
       */
      public class ConsistentHash {
      
          public static void main(String[] args) {
              ServerInfo s1 = new ServerInfo("192.168.1.1");
              ServerInfo s2 = new ServerInfo("192.168.1.2");
              ServerInfo s3 = new ServerInfo("192.168.1.3");
              ServerInfo s4 = new ServerInfo("192.168.1.4");
              ServerInfo s5 = new ServerInfo("192.168.1.5");
              List<ServerInfo> servers = new ArrayList<ServerInfo>();
              servers.add(s1);
              servers.add(s2);
              servers.add(s3);
              servers.add(s4);
              servers.add(s5);
      
              ConsistentHash consistencyHash = ConsistentHash.getInstance(servers);
              for (int i = 0; i < 10; i++) {
                  ServerInfo serverInfo = consistencyHash.hindServer(new Integer(i));
                  System.out.println("数据=i"+"命中了服务器:ip=" + serverInfo.getIp());
              }
      
      
          }
      
          private static ConsistentHash instance;
          private TreeMap<Integer, ServerInfo> circle;
          private List<ServerInfo> servers;
      
          private ConsistentHash(List<ServerInfo> servers) {
              this.servers = servers;
              init(); //init once
          }
      
          public static ConsistentHash getInstance(List<ServerInfo> servers) {
              if (instance == null) {
                  instance = new ConsistentHash(servers);
              }
              return instance;
          }
      
          private void init() {
              circle = new TreeMap<Integer, ServerInfo>();
              for (ServerInfo server : servers) {
                  Integer md5Hash = MD5.md5(server.getIp()).hashCode();
                  circle.put(md5Hash, server);
              }
          }
      
          public void removeServers(List<ServerInfo> servers){
              for(ServerInfo serverInfo : servers){
                  servers.remove(serverInfo);
              }
              init();
          }
      
          public void addSever(ServerInfo serverInfo){
              servers.add(serverInfo);
              init();
          }
      
          /**
           * hint by data hash
           *
           * @param data
           * @return
           */
          public ServerInfo hindServer(Object data) {
              Integer hash = MD5.md5(data.toString()).hashCode(); // 这里对象的hash算法需要改进一下
              SortedMap<Integer, ServerInfo> mapDistrict = circle.tailMap(hash);
              return mapDistrict == null ? circle.get(circle.firstKey()) : circle.get(mapDistrict.firstKey());
          }
      
      }
      
      class ServerInfo {
      
          public ServerInfo(String ip) {
              this.ip = ip;
          }
      
          private String ip;
      
          public String getIp() {
              return ip;
          }
      
          public void setIp(String ip) {
              this.ip = ip;
          }
      }
      
      class MD5 {
          // MD5加密。32位
          public static String md5(String inStr) {
              MessageDigest md5 = null;
              try {
                  md5 = MessageDigest.getInstance("MD5");
              } catch (Exception e) {
                  System.out.println(e.toString());
                  e.printStackTrace();
                  return "";
              }
              char[] charArray = inStr.toCharArray();
              byte[] byteArray = new byte[charArray.length];
      
              for (int i = 0; i < charArray.length; i++)
                  byteArray[i] = (byte) charArray[i];
      
              byte[] md5Bytes = md5.digest(byteArray);
      
              StringBuffer hexValue = new StringBuffer();
      
              for (int i = 0; i < md5Bytes.length; i++) {
                  int val = ((int) md5Bytes[i]) & 0xff;
                  if (val < 16)
                      hexValue.append("0");
                  hexValue.append(Integer.toHexString(val));
              }
      
              return hexValue.toString();
          }
      }
    6. 增加虚拟节点提高散列
      节点的分配是由节点的hash值决定的,当节点数量很少的时候,容易造成数据的分配不均匀,为了解决上述问题,可以将真实节点映射出一些虚拟节点,一个真实节点对应多个虚拟节点,数据以上文提到的方式对应到虚拟节点,进而由虚拟节点映射到具体真实节点,使数据的分布更加散列,如下图所示,原有的node 2节点对应node_2_a和node_2_b节点,object映射到node_2_a或者node_2_b都会对应到真实节点node 2上去。

      同理的,带虚拟节点的hash一致算法的java实现:

      package com.zyq.vitural;
      
      import java.security.MessageDigest;
      import java.util.ArrayList;
      import java.util.List;
      import java.util.SortedMap;
      import java.util.TreeMap;
      
      /**
       * Created with IntelliJ IDEA.
       * User: zouyiq@gmail.com
       * Date: 2018/1/4
       * Time: 22:55
       * To change this template use File | Settings | File Templates.
       */
      public class ConsistentHash {
      
          public static void main(String[] args) {
              ServerInfo s1 = new ServerInfo("192.168.1.1");
              ServerInfo s2 = new ServerInfo("192.168.1.2");
              ServerInfo s3 = new ServerInfo("192.168.1.3");
              ServerInfo s4 = new ServerInfo("192.168.1.4");
              ServerInfo s5 = new ServerInfo("192.168.1.5");
              List<ServerInfo> servers = new ArrayList<ServerInfo>();
              servers.add(s1);
              servers.add(s2);
              servers.add(s3);
              servers.add(s4);
              servers.add(s5);
      
              ConsistentHash consistencyHash = ConsistentHash.getInstance(servers,100);
              for (int i = 0; i < 100; i++) {
                  ServerInfo serverInfo = consistencyHash.hindServer(new Integer(i));
                  System.out.println("数据=i"+"命中了服务器:ip=" + serverInfo.getIp());
              }
          }
      
          private static ConsistentHash instance;
          private TreeMap<Integer, ServerInfo> circle;
          private List<ServerInfo> servers;
          private Integer vituralServerCount;
      
          /**
           * 虚拟server的生成策略
           */
          private void vituralServerListInit(){
              circle = new TreeMap<Integer, ServerInfo>();
              for(ServerInfo serverInfo : servers){
                  Integer orginalHash = MD5.md5(serverInfo.getIp()).hashCode();
                  for (int i = 0; i < vituralServerCount; i++) {
                      Integer vituralHash = MD5.md5(serverInfo.getIp() + "-" + orginalHash + "-" + i).hashCode();
                      circle.put(vituralHash,serverInfo);
                  }
              }
          }
      
          private ConsistentHash(List<ServerInfo> servers,Integer count) {
              this.servers = servers;
              this.vituralServerCount = count;
              vituralServerListInit(); //init once
          }
      
          public static ConsistentHash getInstance(List<ServerInfo> servers,Integer count) {
              if (instance == null) {
                  instance = new ConsistentHash(servers,count);
              }
              return instance;
          }
      
      
      
          public void removeServers(List<ServerInfo> servers){
              for(ServerInfo serverInfo : servers){
                  servers.remove(serverInfo);
              }
              vituralServerListInit();
          }
      
          public void addSever(ServerInfo serverInfo){
              servers.add(serverInfo);
              vituralServerListInit();
          }
      
          /**
           * hint by data hash
           *
           * @param data
           * @return
           */
          public ServerInfo hindServer(Object data) {
              Integer hash = MD5.md5(data.toString()).hashCode(); // 这里对象的hash算法需要改进一下
              SortedMap<Integer, ServerInfo> mapDistrict = circle.tailMap(hash);
              return mapDistrict == null ? circle.get(circle.firstKey()) : circle.get(mapDistrict.firstKey());
          }
      
      }
      
      class ServerInfo {
      
          public ServerInfo(String ip) {
              this.ip = ip;
          }
      
          private String ip;
      
          public String getIp() {
              return ip;
          }
      
          public void setIp(String ip) {
              this.ip = ip;
          }
      }
      
      class MD5 {
          // MD5加密。32位
          public static String md5(String inStr) {
              MessageDigest md5 = null;
              try {
                  md5 = MessageDigest.getInstance("MD5");
              } catch (Exception e) {
                  System.out.println(e.toString());
                  e.printStackTrace();
                  return "";
              }
              char[] charArray = inStr.toCharArray();
              byte[] byteArray = new byte[charArray.length];
      
              for (int i = 0; i < charArray.length; i++)
                  byteArray[i] = (byte) charArray[i];
      
              byte[] md5Bytes = md5.digest(byteArray);
      
              StringBuffer hexValue = new StringBuffer();
      
              for (int i = 0; i < md5Bytes.length; i++) {
                  int val = ((int) md5Bytes[i]) & 0xff;
                  if (val < 16)
                      hexValue.append("0");
                  hexValue.append(Integer.toHexString(val));
              }
      
              return hexValue.toString();
          }
      }
    7. memcached中一致性hash算法的实现细节
      Memcached是一个自由开源的,高性能,分布式内存对象缓存系统。看了下java版client的实现,里面数据对应节点的映射就用到了一致性hash算法,以具体实现为例:
      private volatile TreeMap<Long, MemcachedNode> ketamaNodes; //虚拟节点对应物理节点的映射,key即虚拟节点的hash值

      protected TreeMap<Long, MemcachedNode> getKetamaNodes() {
      return ketamaNodes;
      }
      MemcachedNode getNodeForKey(long hash) {
      final MemcachedNode rv;
      if (!ketamaNodes.containsKey(hash)) {
      // Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5
      // in a lot of places, so I'm doing this myself.
      SortedMap<Long, MemcachedNode> tailMap = getKetamaNodes().tailMap(hash);//通过object的hash值获取虚拟节点map
      if (tailMap.isEmpty()) {
      hash = getKetamaNodes().firstKey();
      } else {
      hash = tailMap.firstKey();//大于object的hash值中,取hash值最小的那个虚拟节点作为命中节点
      }
      }
      rv = getKetamaNodes().get(hash);//通过虚拟节点映射获得真实的物理节点
      return rv;
      }