今天我想先给大家科普下一致性哈希算法这块,因为我下一篇文章关于缓存的高可用需要用到这个,但是又不能直接在里面写太多的代码以及关于一致性hash原理的解读,这样会失去对于缓存高可用的理解而且会造成文章很长,有担心有些朋友还没接触过一致性哈希算法,所以,我就将它单独拎出来讲一下。

什么是一致性哈希

一致性哈希算法在1997年由麻省理工学院提出,是一种特殊的哈希算法,在移除或者添加一个服务器时,能够尽可能小地改变已存在的服务请求与处理请求服务器之间的映射关系 。一致性哈希解决了简单哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的动态伸缩等问题。

一致性哈希算法一般用来干什么

一般我们在项目的负载均衡上要求资源被均匀的分配到所有的服务器节点上,同时,还需要对资源的请求能迅速的路由到具体的节点,例如:我们在做RPC服务的时候,会经常部署多台服务器,然而有时有这样的需求就是,我们希望将同一类型的请求路由到同一台机器上,这个时候就可以用一致性hash算法来实现。

MemCache集群,要求存储数据均匀的放到集群中的各个节点上,访问这些数据时能快速的路由到集群中对应存放该数据的节点上;并且要求增删节点对整个集群的影响很小,不至于有大的动荡造成整体负载的不稳定,这个时候也是可以用一致性hash算法。

一致性哈希算法原理解析

一致性哈希算法核心思想就是,先维护出一个2的32次方整数环【0,2^32-1】,然后将每个节点的计算hash值放到环上。下面通过一个例子来看看 ;

现在有三个节点分别是Node0、Node1、Node3,我们要将多个资源尽可能均匀的分配到这三个节点中,该怎么做呢?

依据一致性hash算法思想,我们需要将资源key进行hash运算,得到的hash值在环上顺时针查找,找到离它最近的节点也就是第一个大于或等于它的节点,这样资源就和节点建立了映射关系。

为何用环来存储节点,还有顺时针查找?

我们要向分配节点第一想到的办法就是取余算法。即现在有3个节点,资源key=7,7%3=1,则选择Node1,key=5,5%3= 2,则选择Node2,key=3,3%3=0,则选择Node0。虽然简单,但有个缺点,如果节点数增加或减少,就会有大量的key不命中,造成请求压力转移,可能对系统整体有很大的影响,甚至发生宕机危险。

而一致性哈希算法增加或减少节点,只会引起很少部分的key不会命中,如下图,增加一个Node4节点,则只会将部分的key值从Node1移到Node4,对集群影响很小。

代码如何实现?

如上,我们已经知道了一致性哈希的原理了也知道它的作用了,那我们该怎么去写代码实现呢?下面我们以java为例写一个一致性哈希实现算法。首先我们得怎么构造这个2的32次方的hash环,当然方法有很多,我这里就直接推荐使用TreeMap这个数据结构,因为TreeMap底层是使用了红黑树结构来存储实体对象的,时间复杂度在O(logN),效率较高。

我们在选择Hash算法上也需要选好,要尽可能的打散开,如果考虑简单的String.HashCode()方法,这个算法的缺点是,相似的字符串如N1(10.0.0.0:91001),N2(10.0.0.0:91002),N3(10.0.0.0:91003),哈希值也很相近,造成的结果是节点在Hash环上分布很紧密,导致大部分Key值落到了N0上,节点资源分布不均。一般我们采用FNV1_32_HASH、KETAMA_HASH等算法,KETAMA_HASH是MemCache集群默认的实现方法,这些算法效果要好得多,会使N0,N1,N2的Hash值更均匀的分布在环上。

那我们先来看看KETAMA_HASH算法实现一致性哈希算法的代码:

import java.io.UnsupportedEncodingException;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.Arrays;

import java.util.Map;

import java.util.TreeMap;

public class ConsistentHashLoadBalance1 {

private TreeMap realNodes = new TreeMap();

private String[] nodes;

public ConsistentHashLoadBalance1(String[] nodes){

this.nodes = Arrays.copyOf(nodes, nodes.length);

initalization();

}

/*** 初始化哈希环* 循环计算每个node名称的哈希值,将其放入treeMap*/

private void initalization(){

for (String nodeName: nodes) {

realNodes.put(hash(nodeName, 0), nodeName);

}

}

/*** 根据资源key选择返回相应的节点名称* @param key* @return 节点名称*/

public String selectNode(String key){

Long hashOfKey = hash(key, 0);

if (! realNodes.containsKey(hashOfKey)) {

//ceilingEntry()的作用是得到比hashOfKey大的第一个Entry Map.Entry entry = realNodes.ceilingEntry(hashOfKey);

if (entry != null)

return entry.getValue();

else

return nodes[0];

}else

return realNodes.get(hashOfKey);

}

private Long hash(String nodeName, int number) {

byte[] digest = md5(nodeName);

return (((long) (digest[3 + number * 4] & 0xFF) << 24)

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

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

| (digest[number * 4] & 0xFF))

& 0xFFFFFFFFL;

}

/*** md5加密** @param str* @return*/

public byte[] md5(String str) {

try {

MessageDigest md = MessageDigest.getInstance("MD5");

md.reset();

md.update(str.getBytes("UTF-8"));

return md.digest();

} catch (NoSuchAlgorithmException e) {

e.printStackTrace();

return null;

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

return null;

}

}

private void printTreeNode(){

if (realNodes != null && ! realNodes.isEmpty()){

realNodes.forEach((hashKey, node) ->

System.out.println(

new StringBuffer(node)

.append(" ==> ")

.append(hashKey)

)

);

}else

System.out.println("Cycle is Empty");

}

public static void main(String[] args){

String[] nodes = new String[]{"192.168.13.1:8080", "192.168.13.2:8080", "192.168.13.3:8080", "192.168.13.4:8080"};

ConsistentHashLoadBalanceNoVirtualNode consistentHash = new ConsistentHashLoadBalanceNoVirtualNode(nodes);

consistentHash.printTreeNode();

}

}

我们来看看输出结果,可以看出,hash结果值还是很开阔的。

192.168.13.2:8080 ==> 596465258

192.168.13.4:8080 ==> 1785851105

192.168.13.1:8080 ==> 2249838119

192.168.13.3:8080 ==> 3292932255

现在我们使用KETAMA_HASH哈希算法,帮我们解决了hash值分布不均匀的问题,但是,目前我们还有个问题,如下图,在Node3节点尚未加入集群之前,数据是均匀分布在{Node0,Node1,Node2}三个节点上的,现在增加了Node3节点后,Node1到Node3节点中间的所有资源从Node2迁移到了Node3上。这样,Node0,Node1存储的资源多,Node2,Node3存储的资源少,资源分布就不均了。

那我们该怎么解决这种问题呢?这里我们就要引入一个叫虚拟节点的概念,其实很简单,就是比方说我现在将真实的节点Node0映射成100个虚拟节点放在环上,同这100个虚拟节点根据KETAMA_HASH哈希环匹配的资源都存到真实节点Node0上,当集群增加节点Node3时,在Hash环上增加Node3拆分的100个虚拟节点,这新增的100个虚拟节点更均匀的分布在了哈希环上,可能承担了{Node0,Node1,Node2}每个节点的部分资源,资源分布仍然保持均匀。

每个真实节点应该拆分成多少个虚拟节点?数量要合适才能保证负载分布的均匀,有一个大致的规律,如下图所示,Y轴表示真实节点的数目,X轴表示需拆分的虚拟节点数目:

真实节点越少,所需阐发的虚拟节点越多,比如有10个真实节点,每个节点所需拆分的虚拟节点个数可能是100~200个,才能达到真正的负载均衡。

下面,我们的代码就需要改造了,需要加入虚拟节点来映射:

import java.io.UnsupportedEncodingException;

import java.security.MessageDigest;

import java.security.NoSuchAlgorithmException;

import java.util.LinkedList;

import java.util.Map;

import java.util.TreeMap;

public class ConsistentHashLoadBalance {

private TreeMap virtualNodes = new TreeMap<>();

private LinkedList nodes;

//每个真实节点对应的虚拟节点数 private final int replicCnt;

public ConsistentHashLoadBalance(LinkedList nodes, int replicCnt){

this.nodes = nodes;

this.replicCnt = replicCnt;

initalization();

}

/*** 初始化哈希环* 循环计算每个node名称的哈希值,将其放入treeMap*/

private void initalization(){

for (String nodeName: nodes) {

for (int i = 0; i < replicCnt/4; i++) {

String virtualNodeName = getNodeNameByIndex(nodeName, i);

for (int j = 0; j < 4; j++) {

virtualNodes.put(hash(virtualNodeName, j), nodeName);

}

}

}

}

private String getNodeNameByIndex(String nodeName, int index){

return new StringBuffer(nodeName)

.append("&&")

.append(index)

.toString();

}

/*** 根据资源key选择返回相应的节点名称* @param key* @return 节点名称*/

public String selectNode(String key){

Long hashOfKey = hash(key, 0);

if (! virtualNodes.containsKey(hashOfKey)) {

Map.Entry entry = virtualNodes.ceilingEntry(hashOfKey);

if (entry != null)

return entry.getValue();

else

return nodes.getFirst();

}else

return virtualNodes.get(hashOfKey);

}

private Long hash(String nodeName, int number) {

byte[] digest = md5(nodeName);

return (((long) (digest[3 + number * 4] & 0xFF) << 24)

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

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

| (digest[number * 4] & 0xFF))

& 0xFFFFFFFFL;

}

/*** md5加密** @param str* @return*/

public byte[] md5(String str) {

try {

MessageDigest md = MessageDigest.getInstance("MD5");

md.reset();

md.update(str.getBytes("UTF-8"));

return md.digest();

} catch (NoSuchAlgorithmException e) {

e.printStackTrace();

return null;

} catch (UnsupportedEncodingException e) {

e.printStackTrace();

return null;

}

}

public void addNode(String node){

nodes.add(node);

String virtualNodeName = getNodeNameByIndex(node, 0);

for (int i = 0; i < replicCnt/4; i++) {

for (int j = 0; j < 4; j++) {

virtualNodes.put(hash(virtualNodeName, j), node);

}

}

}

public void removeNode(String node){

nodes.remove(node);

String virtualNodeName = getNodeNameByIndex(node, 0);

for (int i = 0; i < replicCnt/4; i++) {

for (int j = 0; j < 4; j++) {

virtualNodes.remove(hash(virtualNodeName, j), node);

}

}

}

private void printTreeNode(){

if (virtualNodes != null && ! virtualNodes.isEmpty()){

virtualNodes.forEach((hashKey, node) ->

System.out.println(

new StringBuffer(node)

.append(" ==> ")

.append(hashKey)

)

);

}else

System.out.println("Cycle is Empty");

}

public static void main(String[] args){

LinkedList nodes = new LinkedList<>();

nodes.add("192.168.13.1:8080");

nodes.add("192.168.13.2:8080");

nodes.add("192.168.13.3:8080");

nodes.add("192.168.13.4:8080");

ConsistentHashLoadBalance consistentHash = new ConsistentHashLoadBalance(nodes, 160);

consistentHash.printTreeNode();

}

}

看看输出结果(后面还有):

192.168.13.3:8080 ==> 9681570

192.168.13.1:8080 ==> 9770234

192.168.13.3:8080 ==> 10655171

192.168.13.1:8080 ==> 29484412

192.168.13.1:8080 ==> 32476931

192.168.13.1:8080 ==> 41184104

192.168.13.4:8080 ==> 56379665

192.168.13.2:8080 ==> 58341869

192.168.13.4:8080 ==> 60613368

。。。。

总结,今天我们将如何进行资源均摊引入了一致性哈希算法,并且分享了其原理以及作用,同时,针对增加或减少节点的情况下,会造成资源不均匀且容易发生雪崩的情况,特此在一致性哈希算法中加入了虚拟节点进行了改造,最后通过真实代码的方式展示了我们的一致性hash算法该怎么写。相信这样下一篇文章就很容易了哈。希望对大家有帮助,这样我们下一篇的缓存高可用我觉得大家就好理解了。

如果大家喜欢,或是对大家有所帮助就关注我,我会一直分享业界流行技术方案,让我们共同学习共同进步。

下一篇预告:聊聊我们缓存中的高可用话题

往期精选

一致性哈希算法 mysql_一致性哈希算法,在分布式开发中你必须会写,来看完整代码...相关推荐

  1. 一致性哈希算法 mysql_一致性哈希

    [TOC] 前言 伴随着系统流量的增大,出现了应用集群.在 Redis 中为了保证 Redis 的高可用也为 Redis 搭建了集群对数据进行分槽存放.在 Mysql数据库要存储的量达到一个很高的地步 ...

  2. 经典十大排序算法(含升序降序,基数排序含负数排序)【Java版完整代码】【建议收藏系列】

    经典十大排序算法[Java版完整代码] 写在前面的话 十大排序算法对比 冒泡排序 快速排序 直接选择排序 堆排序 归并排序 插入排序 希尔排序 计数排序 桶排序 基数排序 完整测试类 写在前面的话   ...

  3. 一致性哈希算法 mysql_一致性哈希算法 CARP 原理解析, 附 Golang 实现

    一致性哈希算法 CARP 原理解析, 附 Golang 实现 在后端服务开发的过程中, 遇到了这样一个问题: 需要在 mysql 前面部署 redis 做一层缓存, 要求 redis 是集群部署, 并 ...

  4. 大话分布式理论之二——共识算法与一致性的区别

    [系列目录] 大话分布式理论之二--共识算法与一致性的区别 大话分布式理论之一--从单体到SOA再到微服务 文章目录 共识算法 拜占庭将军问题 分布式理论中的将军们 共识算法-Paxos/Raft等 ...

  5. 【数据结构与算法】字符串匹配 BF算法 RK算法

    单模式串匹配 BF 算法和 RK 算法 BM 算法和 KMP 算法 多模式串匹配算法 Trie 树和 AC 自动机 一.BF 算法 1,BF算法是Brute Force的缩写,中文译作暴力匹配算法,也 ...

  6. vrp 节约算法 c++_数据结构和算法(Golang实现)(8.1)基础知识-前言

    基础知识 学习数据结构和算法.我们要知道一些基础的知识. 一.什么是算法 算法(英文algorithm)这个词在中文里面博大精深,表示算账的方法,也可以表示运筹帷幄的计谋等.在计算机科技里,它表示什么 ...

  7. 一致性哈希 数据迁移 mysql_一致性哈希算法解决分布式数据扩容

    一致性哈希算法在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法,设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似.一致性哈希修正了CARP使用的简 单哈 ...

  8. 一致性哈希算法在分布式缓存中的应用

    一.应用场景 假设我们有一个网站,最近发现随着流量增加,服务器压力越来越大,之前直接读写数据库的方式不太给力了,于是我们想引入Redis作为缓存机制.现在我们一共有三台机器可以作为Redis服务器,如 ...

  9. 【算法】一致性哈希算法原理详解

    一.普通 hash 算法 (取模算法): 在了解一致性哈希算法之前,我们先了解一下缓存中的一个应用场景,了解了这个应用场景之后,再来理解一致性哈希算法,就容易多了,也更能体现出一致性哈希算法的优点,那 ...

最新文章

  1. 定个目标|建立自己的技术知识体系
  2. 计算机集成制造 pdf,计算机集成制造系统方法论9412536127640μ.pdf
  3. 微软MCP之SharePoint2007考试试题(Microsoft 70-542)
  4. MySQL中的前缀索引
  5. Flask 框架中 上下文基础理念,包括cookie,session存储方法,requset属性,current_app模块和g模块...
  6. mysql 修改表的编码_Mysql表编码查看修改
  7. 水果编曲软件除了做电音还能做什么
  8. 四川省副高级职称计算机考试试题,四川省职称计算机考试题库.doc
  9. 读取Xilinx FPGA芯片的唯一ID号DNA
  10. 科技热点周刊|GitLab 上市、LinkedIn 中国停止运营、Visual Studio 2022 正式版将发布
  11. Linux Centos装机全过程总结,小白入(持续更新中...)
  12. 【总结】1111- 如何搞定Banner背景自动换色的功能?
  13. navigationBar 标题字体颜色设置
  14. influxdb删除错误DB contains shards using both inmem and tsi1 indexes.
  15. 学习完美方块小游戏(cocos creator)
  16. “深度学习“汉字国标码GB2312
  17. 解析html网页原理,深入浅出浏览器渲染原理
  18. CF刷题-Codeforces Round #481-F. Mentors
  19. python程序开发的各个阶段_python基础----面向对象的程序设计(五个阶段、对小白的忠告、关于OOP常用术语)、类、对象...
  20. Python实现VIIRS气溶胶产品重投影-类GLT实现

热门文章

  1. Python学习Day7
  2. SSH反向代理转发至内网msf
  3. 分析报告生产器使用问题
  4. WorkSbell第10周2017
  5. 基于socket编程的多人聊天室
  6. 最新Android ADT, SDK, SDK_tool等官方下载说明(及时更新)
  7. ubuntu 设置保护色
  8. 复制和删除文件夹所有内容
  9. bat批量修改及替换文件内容
  10. HDR概念 (二十八)