先来先服务算法代码_一致性哈希算法编写
今天我想先給大家科普下一致性哈希算法這塊,因?yàn)槲蚁乱黄恼玛P(guān)于緩存的高可用需要用到這個(gè),但是又不能直接在里面寫太多的代碼以及關(guān)于一致性hash原理的解讀,這樣會(huì)失去對(duì)于緩存高可用的理解而且會(huì)造成文章很長(zhǎng),有擔(dān)心有些朋友還沒(méi)接觸過(guò)一致性哈希算法,所以,我就將它單獨(dú)拎出來(lái)講一下。
什么是一致性哈希
一致性哈希算法在1997年由麻省理工學(xué)院提出,是一種特殊的哈希算法,在移除或者添加一個(gè)服務(wù)器時(shí),能夠盡可能小地改變已存在的服務(wù)請(qǐng)求與處理請(qǐng)求服務(wù)器之間的映射關(guān)系 。一致性哈希解決了簡(jiǎn)單哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的動(dòng)態(tài)伸縮等問(wèn)題。
一致性哈希算法一般用來(lái)干什么
一般我們?cè)陧?xiàng)目的負(fù)載均衡上要求資源被均勻的分配到所有的服務(wù)器節(jié)點(diǎn)上,同時(shí),還需要對(duì)資源的請(qǐng)求能迅速的路由到具體的節(jié)點(diǎn),例如:
一致性哈希算法原理解析
一致性哈希算法核心思想就是,先維護(hù)出一個(gè)2的32次方整數(shù)環(huán)【0,2^32-1】,然后將每個(gè)節(jié)點(diǎn)的計(jì)算hash值放到環(huán)上。下面通過(guò)一個(gè)例子來(lái)看看 ;
現(xiàn)在有三個(gè)節(jié)點(diǎn)分別是Node0、Node1、Node3,我們要將多個(gè)資源盡可能均勻的分配到這三個(gè)節(jié)點(diǎn)中,該怎么做呢?
依據(jù)一致性hash算法思想,我們需要將資源key進(jìn)行hash運(yùn)算,得到的hash值在環(huán)上順時(shí)針查找,找到離它最近的節(jié)點(diǎn)也就是第一個(gè)大于或等于它的節(jié)點(diǎn),這樣資源就和節(jié)點(diǎn)建立了映射關(guān)系。
為何用環(huán)來(lái)存儲(chǔ)節(jié)點(diǎn),還有順時(shí)針查找?
我們要向分配節(jié)點(diǎn)第一想到的辦法就是取余算法。即現(xiàn)在有3個(gè)節(jié)點(diǎn),資源key=7,7%3=1,則選擇Node1,key=5,5%3= 2,則選擇Node2,key=3,3%3=0,則選擇Node0。雖然簡(jiǎn)單,但有個(gè)缺點(diǎn),如果節(jié)點(diǎn)數(shù)增加或減少,就會(huì)有大量的key不命中,造成請(qǐng)求壓力轉(zhuǎn)移,可能對(duì)系統(tǒng)整體有很大的影響,甚至發(fā)生宕機(jī)危險(xiǎn)。
而一致性哈希算法增加或減少節(jié)點(diǎn),只會(huì)引起很少部分的key不會(huì)命中,如下圖,增加一個(gè)Node4節(jié)點(diǎn),則只會(huì)將部分的key值從Node1移到Node4,對(duì)集群影響很小。
代碼如何實(shí)現(xiàn)?
如上,我們已經(jīng)知道了一致性哈希的原理了也知道它的作用了,那我們?cè)撛趺慈懘a實(shí)現(xiàn)呢?下面我們以java為例寫一個(gè)一致性哈希實(shí)現(xiàn)算法。
那我們先來(lái)看看KETAMA_HASH算法實(shí)現(xiàn)一致性哈希算法的代碼:
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<Long, String> realNodes = new TreeMap();private String[] nodes;public ConsistentHashLoadBalance1(String[] nodes){this.nodes = Arrays.copyOf(nodes, nodes.length);initalization();}/*** 初始化哈希環(huán)* 循環(huán)計(jì)算每個(gè)node名稱的哈希值,將其放入treeMap*/private void initalization(){for (String nodeName: nodes) {realNodes.put(hash(nodeName, 0), nodeName);}}/*** 根據(jù)資源key選擇返回相應(yīng)的節(jié)點(diǎn)名稱* @param key* @return 節(jié)點(diǎn)名稱*/public String selectNode(String key){Long hashOfKey = hash(key, 0);if (! realNodes.containsKey(hashOfKey)) {//ceilingEntry()的作用是得到比hashOfKey大的第一個(gè)EntryMap.Entry<Long, String> entry = realNodes.ceilingEntry(hashOfKey);if (entry != null)return entry.getValue();elsereturn nodes[0];}elsereturn 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)));}elseSystem.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();} }我們來(lái)看看輸出結(jié)果,可以看出,hash結(jié)果值還是很開闊的。
192.168.13.2:8080 ==> 596465258
192.168.13.4:8080 ==> 1785851105
192.168.13.1:8080 ==> 2249838119
192.168.13.3:8080 ==> 3292932255
現(xiàn)在我們使用KETAMA_HASH哈希算法,幫我們解決了hash值分布不均勻的問(wèn)題,但是,目前我們還有個(gè)問(wèn)題,如下圖,在Node3節(jié)點(diǎn)尚未加入集群之前,數(shù)據(jù)是均勻分布在{Node0,Node1,Node2}三個(gè)節(jié)點(diǎn)上的,現(xiàn)在增加了Node3節(jié)點(diǎn)后,Node1到Node3節(jié)點(diǎn)中間的所有資源從Node2遷移到了Node3上。這樣,Node0,Node1存儲(chǔ)的資源多,Node2,Node3存儲(chǔ)的資源少,資源分布就不均了。
那我們?cè)撛趺唇鉀Q這種問(wèn)題呢?這里我們就要引入一個(gè)叫虛擬節(jié)點(diǎn)的概念,其實(shí)很簡(jiǎn)單,就是比方說(shuō)我現(xiàn)在將真實(shí)的節(jié)點(diǎn)Node0映射成100個(gè)虛擬節(jié)點(diǎn)放在環(huán)上,同這100個(gè)虛擬節(jié)點(diǎn)根據(jù)KETAMA_HASH哈希環(huán)匹配的資源都存到真實(shí)節(jié)點(diǎn)Node0上,當(dāng)集群增加節(jié)點(diǎn)Node3時(shí),在Hash環(huán)上增加Node3拆分的100個(gè)虛擬節(jié)點(diǎn),這新增的100個(gè)虛擬節(jié)點(diǎn)更均勻的分布在了哈希環(huán)上,可能承擔(dān)了{(lán)Node0,Node1,Node2}每個(gè)節(jié)點(diǎn)的部分資源,資源分布仍然保持均勻。
每個(gè)真實(shí)節(jié)點(diǎn)應(yīng)該拆分成多少個(gè)虛擬節(jié)點(diǎn)?數(shù)量要合適才能保證負(fù)載分布的均勻,有一個(gè)大致的規(guī)律,如下圖所示,Y軸表示真實(shí)節(jié)點(diǎn)的數(shù)目,X軸表示需拆分的虛擬節(jié)點(diǎn)數(shù)目:
真實(shí)節(jié)點(diǎn)越少,所需闡發(fā)的虛擬節(jié)點(diǎn)越多,比如有10個(gè)真實(shí)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)所需拆分的虛擬節(jié)點(diǎn)個(gè)數(shù)可能是100~200個(gè),才能達(dá)到真正的負(fù)載均衡。
下面,我們的代碼就需要改造了,需要加入虛擬節(jié)點(diǎn)來(lái)映射:
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<Long, String> virtualNodes = new TreeMap<>();private LinkedList<String> nodes;//每個(gè)真實(shí)節(jié)點(diǎn)對(duì)應(yīng)的虛擬節(jié)點(diǎn)數(shù)private final int replicCnt;public ConsistentHashLoadBalance(LinkedList<String> nodes, int replicCnt){this.nodes = nodes;this.replicCnt = replicCnt;initalization();}/*** 初始化哈希環(huán)* 循環(huán)計(jì)算每個(gè)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();}/*** 根據(jù)資源key選擇返回相應(yīng)的節(jié)點(diǎn)名稱* @param key* @return 節(jié)點(diǎn)名稱*/public String selectNode(String key){Long hashOfKey = hash(key, 0);if (! virtualNodes.containsKey(hashOfKey)) {Map.Entry<Long, String> entry = virtualNodes.ceilingEntry(hashOfKey);if (entry != null)return entry.getValue();elsereturn nodes.getFirst();}elsereturn 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)));}elseSystem.out.println("Cycle is Empty");}public static void main(String[] args){LinkedList<String> 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();} }看看輸出結(jié)果(后面還有):
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
。。。。
總結(jié),今天我們將如何進(jìn)行資源均攤引入了一致性哈希算法,并且分享了其原理以及作用,同時(shí),針對(duì)增加或減少節(jié)點(diǎn)的情況下,會(huì)造成資源不均勻且容易發(fā)生雪崩的情況,特此在一致性哈希算法中加入了虛擬節(jié)點(diǎn)進(jìn)行了改造,最后通過(guò)真實(shí)代碼的方式展示了我們的一致性hash算法該怎么寫。相信這樣下一篇文章就很容易了哈。希望對(duì)大家有幫助,這樣我們下一篇的緩存高可用我覺(jué)得大家就好理解了。
如果大家喜歡,或是對(duì)大家有所幫助就關(guān)注我,我會(huì)一直分享業(yè)界流行技術(shù)方案,讓我們共同學(xué)習(xí)共同進(jìn)步。
下一篇預(yù)告:聊聊我們緩存中的高可用話題
往期精選
你一定要掌握這種緩存讀寫策略,開發(fā)必備
消息中間件能干什么?RabbitMQ、Kafka、RocketMQ正確選型姿勢(shì)
NoSql數(shù)據(jù)庫(kù),是怎么解決我們高并發(fā)場(chǎng)景下MySql表現(xiàn)的不足
數(shù)據(jù)庫(kù)分庫(kù)分表,手把手教你怎么去動(dòng)態(tài)擴(kuò)容索容
每天百萬(wàn)交易的支付系統(tǒng),生產(chǎn)環(huán)境該怎么設(shè)置JVM堆內(nèi)存大小
你的成神之路我已替你鋪好,沒(méi)鋪你來(lái)捶我
總結(jié)
以上是生活随笔為你收集整理的先来先服务算法代码_一致性哈希算法编写的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python 通登录银行_Python3
- 下一篇: next数组_【阿里面试热身题】数组去重