一致性哈希算法及其实现

一致性哈希算法及其实现一致性哈希算法及其实现(ConsistentHashing)一,一致性哈希算法的原理1,一致性哈希算法诞生的背景   技术和业务是相互推动,共同前进的。一致性哈希算法的产生也源于业务的需求。随着业务的增长,一台单机已经不能满足业务的需要,分布式架构应运而生。分布式环境下,多台机器需要协同作业,如果保证数据在分布式环境下的一致性,就成为了亟待解决的问题。一致性哈希算法,就是为了解决多台…

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。

Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺

一致性哈希算法及其实现(Consistent Hashing)

一,一致性哈希算法的原理

1,一致性哈希算法诞生的背景
    技术和业务是相互推动,共同前进的。一致性哈希算法的产生也源于业务的需求。随着业务的增长,一台单机
已经不能满足业务的需要,分布式架构应运而生。分布式环境下,多台机器需要协同作业,如果保证数据在分布式
环境下的一致性,就成为了亟待解决的问题。一致性哈希算法,就是为了解决多台机器,在动态增删的情况下,能够
最大限度地保证信息的一致性。
    一致性哈希算法是一种分布式哈希算法,设计目标是为了解决互联网中的热点(Hot spot)问题。一致性哈希算法
设计初衷和CARP十分类似。CARP,即Composition/Aggregation Principle,组合/聚合原则。CARP的目标之一,是为
了改善服务的可用性。在多台服务器环境下,进行故障转移,提高系统的可用性。一致性哈希修正了CARP使用的简单
哈希算法带来的问题,使得分布式哈希(DHT)可以在P2P环境中真正得到应用。 

2,一致性哈希算法实现参考标准
    设计一致性hash算法,一般需要遵循以下几个标准:
2.1、平衡性(Balance):平衡性不仅仅指的是平均分配,可以理解为一种加权的平均,根据每台服务器的能力,把任务
分配下去,充分利用每台机器的资源。
2.2、单调性(Monotonicity):如何理解单调性?是一个难点。网上的说法基本上都是“拿来主义”,并没有把单调性的
概念说清楚。如果仅仅从概念上来说单调性,很难说清楚。我们可以换一个角度来解读“单调性”,从单调性要解决的
问题入手,解读“单调性”,或者说从单调性的功能入手,解读“单调性”。在动态变化的分布式环境中,增加服务器节点
和移除服务器节点是最常见的操作。如果我们采用简单的哈希算法,比如使用节点的IP的哈希值hash%节点数N,做为哈
希值,映射到节点上。那么,一旦节点数发生了变化,所以的哈希值都会失效。单调性就是为了解决这个问题的。
2.3、分散性(Spread):在分布式环境中,节点A有可能看不到其他所有的N-1个节点,只看到其中的一部分节点。当节
点A将数据映射到其他节点时,由于不同节点所见的集群范围有可能不同,从而导致哈希的结果不一致,最终相同的
数据被不同的节点映射到不同的内存中。这种情况显然是应该避免的,因为它导致相同的数据被存储到不同的节点,降低
了系统存储的效率。分散性目前不是很理解,为什么相同的数据会映射到不同的节点呢?留着以后研究。 
2.4、负载(Load):负载的概念看了网上的说法,也不是很明白,留着以后研究。

二,一致性哈希算法的创新点

    一致性哈希算法的实现过程,其实就是为了解决上述问题的过程。我们这里不按照网上的方式进行枯燥的讲解,而是
通过对比,进行有针对性的讲解。一致性哈希算法,在不同的系统环境下,具有不同的实现方式。但是,实现的大致过程
还是一致的。

1,静态映射 –> 动态映射
    普通的哈希算法,比如上面提到的(hash % N),由于数据和节点是静态绑定的。也就是说,进行哈希运算后,数据
和节点之间的关系就确定了。一旦节点数发生变化,所有的哈希都失效了。一致性哈希算法,是如何解决这个问题的呢?
一致性哈希算法引入了环的概念,并且最关键的创新点是:将节点的分配和数据的分配,拆分成了2个独立的过程。数据
和节点的关联,不是通过哈希算法直接建立起来的。这样数据和节点就相对独立了,节点A的变化,并不会影响到整个分布
式系统,因为此时不需要对所有数据进行哈希运算。
    一致性哈希算法的进步之处在于,把数据和节点的关联,从“静态”变成了“动态”。

2,顺时针就近查找节点
    一致性哈希算法是怎么把数据和节点关联起来的呢?在节点和数据都哈希到圆环上以后,数据通过顺时针方向查找的
方式,与节点建立关联。数据把顺时针找到的第一个节点作为自己的存储位置,这样一来,数据和节点就完美的关联起来
了。

三,一致性哈希算法面对的问题

    一致性哈希算法解决了普通哈希算法不能解决的问题,但是一致性哈希算法也存在一定的缺陷。在节点A挂掉的情况下,
映射到节点A上的数据,会受到影响。因为之前映射到A节点的数据,现在按照顺时针查找,映射到了节点A的下一个节点。
同样的,在增加一个节点时,也会影响一部分数据。
    一致性哈希算法的另一个缺陷是,当集群中的节点数量很少时,会造成数据倾斜。数据倾斜的问题,可以通过虚拟节点
的方式来解决。在虚拟节点和实际节点之间再增加一次映射。

    总之,相比于普通的哈希算法,一致性哈希算法对于节点的动态增删,具有一定的容错性和可扩展性。

 

 

 
  1. /**

  2. * MurMurHash算法,是非加密HASH算法,性能很高,

  3. * 比传统的CRC32,MD5,SHA-1(这两个算法都是加密HASH算法,复杂度本身就很高,带来的性能上的损害也不可避免)

  4. * 等HASH算法要快很多,而且据说这个算法的碰撞率很低.

  5. * http://murmurhash.googlepages.com/

  6. */

  7. private Long hash(String key) {

  8.  
  9. ByteBuffer buf = ByteBuffer.wrap(key.getBytes());

  10. int seed = 0x1234ABCD;

  11.  
  12. ByteOrder byteOrder = buf.order();

  13. buf.order(ByteOrder.LITTLE_ENDIAN);

  14.  
  15. long m = 0xc6a4a7935bd1e995L;

  16. int r = 47;

  17.  
  18. long h = seed ^ (buf.remaining() * m);

  19.  
  20. long k;

  21. while (buf.remaining() >= 8) {

  22. k = buf.getLong();

  23.  
  24. k *= m;

  25. k ^= k >>> r;

  26. k *= m;

  27.  
  28. h ^= k;

  29. h *= m;

  30. }

  31.  
  32. if (buf.remaining() > 0) {

  33. ByteBuffer finish = ByteBuffer.allocate(8).order(

  34. ByteOrder.LITTLE_ENDIAN);

  35. // for big-endian version, do this first:

  36. // finish.position(8-buf.remaining());

  37. finish.put(buf).rewind();

  38. h ^= finish.getLong();

  39. h *= m;

  40. }

  41.  
  42. h ^= h >>> r;

  43. h *= m;

  44. h ^= h >>> r;

  45.  
  46. buf.order(byteOrder);

  47. return h;

  48. }

四,一致性哈希算法的java实现

 

 

 
  1. package redis.cn;

  2. import java.nio.charset.Charset;

  3. import java.util.List;

  4. import java.util.SortedMap;

  5. import java.util.TreeMap;

  6. import com.google.common.hash.HashFunction;

  7. import com.google.common.hash.Hashing;

  8. public class ConsistentHash {

  9. // ------------------ 一致性哈希算法的java实现 ------------------

  10. private SortedMap<Long,String> ketamaNodes = new TreeMap<Long,String>();

  11. private int numberOfReplicas = 1024;

  12. // 这里使用了谷歌的jar包 -- guava-18.0.jar

  13. private HashFunction hashFunction = Hashing.md5();

  14. private List<String> nodes;

  15. private volatile boolean init = false; //标志是否初始化完成

  16. // 有参数构造函数

  17. public ConsistentHash(int numberOfReplicas,List<String> nodes){

  18. this.numberOfReplicas = numberOfReplicas;

  19. this.nodes = nodes;

  20. init();

  21. }

  22. // 根据key的哈希值,找到最近的一个节点(服务器)

  23. public String getNodeByKey(String key){

  24. if(!init)

  25. throw new RuntimeException("init uncomplete...");

  26. // 注意,这里是NIO包 java.nio.charset.Charset

  27. byte[] digest = hashFunction.hashString(key, Charset.forName("UTF-8")).asBytes();

  28. long hash = hash(digest,0);

  29. //如果找到这个节点,直接取节点,返回

  30. if(!ketamaNodes.containsKey(hash)){

  31. //得到大于当前key的那个子Map,然后从中取出第一个key,就是大于且离它最近的那个key

  32. SortedMap<Long,String> tailMap = ketamaNodes.tailMap(hash);

  33. if(tailMap.isEmpty()){

  34. hash = ketamaNodes.firstKey();

  35. }else{

  36. hash = tailMap.firstKey();

  37. }

  38.  
  39. }

  40. return ketamaNodes.get(hash);

  41. }

  42. // 新增节点

  43. public synchronized void addNode(String node){

  44. init = false;

  45. nodes.add(node);

  46. init();

  47. }

  48.  
  49. private void init(){

  50. //对所有节点,生成numberOfReplicas个虚拟节点

  51. for(String node:nodes){

  52. //每四个虚拟节点为1组

  53. for(int i=0;i<numberOfReplicas/4;i++){

  54. //为这组虚拟结点得到惟一名称

  55. byte[] digest = hashFunction.hashString(node+i, Charset.forName("UTF-8")).asBytes();

  56. //Md5是一个16字节长度的数组,将16字节的数组每四个字节一组,分别对应一个虚拟结点,这就是为什么上面把虚拟结点四个划分一组的原因

  57. for(int h=0;h<4;h++){

  58. Long k = hash(digest,h);

  59. ketamaNodes.put(k,node);

  60. }

  61. }

  62. }

  63. init = true;

  64. }

  65.  
  66. public void printNodes(){

  67. for(Long key:ketamaNodes.keySet()){

  68. System.out.println(ketamaNodes.get(key));

  69. }

  70. }

  71. // 哈希算法

  72. public static long hash(byte[] digest, int nTime)

  73. {

  74. long rv = ((long)(digest[3 + nTime * 4] & 0xFF) << 24)

  75. | ((long)(digest[2 + nTime * 4] & 0xFF) << 16)

  76. | ((long)(digest[1 + nTime * 4] & 0xFF) << 8)

  77. | ((long)digest[0 + nTime * 4] & 0xFF);

  78. return rv;

  79. }

  80. }

五,一致性哈希算法在redis中的应用

  Redis本身不支持集群,所以需要借助API或者其他第三方产品,来实现集群部署。当然,也可以借助一致性哈希算法来
实现Redis集群。Memcached对大家应该不陌生,通过把Key映射到Memcached Server上,实现快速读取。我们可以动态对其节点
增加,并未影响之前已经映射到内存的Key与memcached Server之间的关系,这就是因为使用了一致性哈希算法。Memcached的
哈希策略是在客户端实现的,因此不同的客户端实现有区别,以Spymemcache、Xmemcache为例,都是使用了KETAMA作为其实现。
    实现redis分布式集群,可以参考下面几种思路:
    * 使用jedis
    * 自己实现一致性哈希算法;

1,jedis
    jedis是redis客户端API。Redis-server端并没有sharding方法,但是我们可以使用jedis来实现分布式。jedis使用了一种
叫做sharding的思想。
    什么是sharding呢?简单的来说,就是数据库“分片”。sharding的核心理念就是将数据按照一定的策略”分散”存储在集群
中不同的物理机器上,从根本上来讲,实现了”大数据”分布式存储,体现了”集群”的概念。比如1亿条数据,我们可以根据数据
的hashcode,把数据散列存储在5个物理机器上。
    sharding的实现,也是基于一致性哈希算法。我们先来看一下sharding实现的关键源代码。
    1.1 hashcode取值:源码来自redis.clients.util.Hashing。Jedis中默认的hash算法是MD5,即我们熟悉的第五代信息摘要
算法:Message Digest Algorithm 5 。

 

 
  1. //少量优化性能

  2. public ThreadLocal<MessageDigest> md5Holder = new ThreadLocal<MessageDigest>();

  3. public static final Hashing MD5 = new Hashing() {

  4. public long hash(String key) {

  5. return hash(SafeEncoder.encode(key));

  6. }

  7. // sharding使用的哈希算法是MD5

  8. public long hash(byte[] key) {

  9. try {

  10. if (md5Holder.get() == null) {

  11. md5Holder.set(MessageDigest.getInstance("MD5"));

  12. }

  13. }

  14. catch (NoSuchAlgorithmException e) {

  15. throw new IllegalStateException("++++ no md5 algorythm found");

  16. }

  17. MessageDigest md5 = md5Holder.get();

  18. md5.reset();

  19. md5.update(key);

  20. //获得MD5字节序列

  21. byte[] bKey = md5.digest();

  22. //前四个字节作为计算参数,最终获得一个32位int值.

  23. //此种计算方式,能够确保key的hash值更加“随机”/“离散”

  24. //如果hash值过于密集,不利于一致性hash的实现(特别是有“虚拟节点”设计时)

  25. long res = ((long) (bKey[3] & 0xFF) << 24)

  26. | ((long) (bKey[2] & 0xFF) << 16)

  27. | ((long) (bKey[1] & 0xFF) << 8)

  28. | (long) (bKey[0] & 0xFF);

  29. return res;

  30. }

  31. };

    1.2 node构建过程(redis.clients.util.Sharded):

 

 
  1. //shards列表为客户端提供了所有redis-server配置信息,包括:ip,port,weight,name

  2. //其中weight为权重,将直接决定“虚拟节点”的“比例”(密度),权重越高,在存储是被hash命中的概率越高

  3. //--其上存储的数据越多。

  4. //其中name为“节点名称”,jedis使用name作为“节点hash值”的一个计算参数。

  5. //---

  6. //一致性hash算法,要求每个“虚拟节点”必须具备“hash值”,每个实际的server可以有多个“虚拟节点”(API级别)

  7. //其中虚拟节点的个数= “逻辑区间长度” * weight,每个server的“虚拟节点”将会以“hash”的方式分布在全局区域中

  8. //全局区域总长为2^32.每个“虚拟节点”以hash值的方式映射在全局区域中。

  9. // 环形:0-->vnode1(:1230)-->vnode2(:2800)-->vnode3(400000)---2^32-->0

  10. //所有的“虚拟节点”将按照其”节点hash“顺序排列(正序/反序均可),因此相邻两个“虚拟节点”之间必有hash值差,

  11. //那么此差值,即为前一个(或者后一个,根据实现而定)“虚拟节点”所负载的数据hash值区间。

  12. //比如hash值为“2000”的数据将会被vnode1所接受。

  13. private void initialize(List<S> shards){

  14. //虚拟节点,采取TreeMap存储:排序,二叉树

  15. nodes = new TreeMap<Long, S>();

  16. for (int i = 0; i != shards.size(); ++i) {

  17. final S shardInfo = shards.get(i);

  18. if (shardInfo.getName() == null)

  19. //当没有设置“name”是,将“SHARD-NODE”作为“虚拟节点”hash值计算的参数

  20. //"逻辑区间步长"为160,为什么呢??

  21. //最终多个server的“虚拟节点”将会交错布局,不一定非常均匀。

  22. for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {

  23. nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);

  24. }

  25. else

  26. for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {

  27. nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);

  28. }

  29. resources.put(shardInfo, shardInfo.createResource());

  30. }

  31. }

    1.3,node选择方式:

 

 
  1. public R getShard(String key) {

  2. return resources.get(getShardInfo(key));

  3. }

  4. public S getShardInfo(byte[] key) {

  5. //获取>=key的“虚拟节点”的列表

  6. SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));

  7. //如果不存在“虚拟节点”,则将返回首节点。

  8. if (tail.size() == 0) {

  9. return nodes.get(nodes.firstKey());

  10. }

  11. //如果存在,则返回符合(>=key)条件的“虚拟节点”的第一个节点

  12. return tail.get(tail.firstKey());

  13. }

    Jedis sharding模式下,如果某个server失效,客户端并不会删除此sharding,所以如果访问此sharding将会抛出异常。
这是为了保持所有的客户端数据视图一致性。你可能希望动态的一致性hash拓扑结构(即如果某个shard失效,sharding结构
则重新调整,失效的sharding上的数据则被hash到其他sharding上),但是很遗憾,SharedJedis客户端无法支持,如果非要
支持,则需要巨大的代码调整,而且还需要引入额外的拓扑自动发现机制。(参看:redis cluster架构,已提供此问题的完
善解决方案)。不过,在持久存储的情况下,我们可以使用”强hash”分片,则需要重写其Hash算法。强hash算法下,如果某个虚
拟节点所在的物理server故障,将导致数据无法访问(读取/存储),即不会从虚拟节点列表中删除那些失效的server。
    对于jedis如果重写了一致性哈希算法,你需要考虑以下几个方面:
    1) 虚拟节点hash是否相对均匀 
    2) 数据的hash值分布是否均匀 
    3) 虚拟节点在“全局”是否散列均匀。
    如果设计不良,很有可能导致数据在server上分布不均,而失去了sharding的本身意义。

2,java中使用jedis的demo

 

 
  1. package redis.cn;

  2. import java.util.ArrayList;

  3. import java.util.List;

  4. import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

  5. import redis.clients.jedis.JedisShardInfo;

  6. import redis.clients.jedis.ShardedJedis;

  7. import redis.clients.jedis.ShardedJedisPool;

  8. /**

  9. * @author yangcq

  10. * @category jedis也是一致性哈希算法的一种实现。搭建redis分布式集群,可以使用jedis。

  11. */

  12. public class ShardedRedis {

  13.  
  14. // 除了jdk自带的工具包以后,还需要导入下面2个jar包

  15. // commons-pool2-2.0.jar

  16. // jedis-2.4.2.jar

  17.  
  18. public static void main(String[] args){

  19. // jedis配置参数

  20. GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();

  21. genericObjectPoolConfig.setMaxTotal(1000);

  22. genericObjectPoolConfig.setMaxIdle(500);

  23.  
  24. List<JedisShardInfo> jedisShardInfoList = new ArrayList<JedisShardInfo>();

  25. JedisShardInfo jedisShardInfo1 = new JedisShardInfo("127.0.0.1",1234);

  26. JedisShardInfo jedisShardInfo2 = new JedisShardInfo("127.0.0.1",1235);

  27. JedisShardInfo jedisShardInfo3 = new JedisShardInfo("127.0.0.1",1236);

  28. jedisShardInfoList.add(jedisShardInfo1);

  29. jedisShardInfoList.add(jedisShardInfo2);

  30. jedisShardInfoList.add(jedisShardInfo3);

  31.  
  32. ShardedJedisPool shardedJedisPool = new ShardedJedisPool(genericObjectPoolConfig,jedisShardInfoList);

  33.  
  34. set("key1","value1",shardedJedisPool);

  35. set("key2","value2",shardedJedisPool);

  36. set("key3","value3",shardedJedisPool);

  37. set("key4","value4",shardedJedisPool);

  38. set("key5","value5",shardedJedisPool);

  39.  
  40. // jedis隐藏了实现一致性哈希算法的细节,只是给我们提供了简单的接口调用,就可以实现redis分布式集群的搭建

  41. // 那么jedis到底是如何实现一致性哈希算法的呢?

  42. }

  43.  
  44. public static void set(String key,String value,ShardedJedisPool pool){

  45. // 从共享资源池中获取redis实例

  46. ShardedJedis shardedJedis = pool.getResource();

  47. // 赋值

  48. shardedJedis.set(key,value);

  49. pool.returnResource(shardedJedis);

  50. }

  51. }

——————————————————

参考源码:

Jedis是通过ShardedJedis向redis集群写入的数据,ShardedJedis中的关键方法:

 

 
  1. public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {

  2. this.algo = algo;

  3. this.tagPattern = tagPattern;

  4. initialize(shards);

  5. }

  6.  
  7. //初始化哈希环

  8. private void initialize(List<S> shards) {

  9. nodes = new TreeMap<Long, S>();

  10.  
  11. for (int i = 0; i != shards.size(); ++i) {

  12. final S shardInfo = shards.get(i);

  13. if (shardInfo.getName() == null)

  14. for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {

  15. nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n),

  16. shardInfo);

  17. }

  18. else

  19. for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {

  20. nodes.put(

  21. this.algo.hash(shardInfo.getName() + "*"

  22. + shardInfo.getWeight() + n), shardInfo);

  23. }

  24. resources.put(shardInfo, shardInfo.createResource());

  25. }

  26. }

  27.  
  28. //将key,value存储到相应的shard

  29. public String set(String key, String value) {

  30. Jedis j = getShard(key);

  31. return j.set(key, value);

  32. }

  33.  
  34. public R getShard(String key) {

  35. return resources.get(getShardInfo(key));

  36. }

  37.  
  38. //根据key获取shard

  39. public S getShardInfo(byte[] key) {

  40. SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key));

  41. if (tail.isEmpty()) {

  42. return nodes.get(nodes.firstKey());

  43. }

  44. return tail.get(tail.firstKey());

  45. }

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/164583.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号