先来先服务算法代码_一致性哈希算法编写
今天我想先給大家科普下一致性哈希算法這塊,因為我下一篇文章關于緩存的高可用需要用到這個,但是又不能直接在里面寫太多的代碼以及關于一致性hash原理的解讀,這樣會失去對于緩存高可用的理解而且會造成文章很長,有擔心有些朋友還沒接觸過一致性哈希算法,所以,我就將它單獨拎出來講一下。
什么是一致性哈希
一致性哈希算法在1997年由麻省理工學院提出,是一種特殊的哈希算法,在移除或者添加一個服務器時,能夠盡可能小地改變已存在的服務請求與處理請求服務器之間的映射關系 。一致性哈希解決了簡單哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的動態伸縮等問題。
一致性哈希算法一般用來干什么
一般我們在項目的負載均衡上要求資源被均勻的分配到所有的服務器節點上,同時,還需要對資源的請求能迅速的路由到具體的節點,例如:
一致性哈希算法原理解析
一致性哈希算法核心思想就是,先維護出一個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為例寫一個一致性哈希實現算法。
那我們先來看看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<Long, String> 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大的第一個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();} }我們來看看輸出結果,可以看出,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<Long, String> virtualNodes = new TreeMap<>();private LinkedList<String> nodes;//每個真實節點對應的虛擬節點數private final int replicCnt;public ConsistentHashLoadBalance(LinkedList<String> 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<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();} }看看輸出結果(后面還有):
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算法該怎么寫。相信這樣下一篇文章就很容易了哈。希望對大家有幫助,這樣我們下一篇的緩存高可用我覺得大家就好理解了。
如果大家喜歡,或是對大家有所幫助就關注我,我會一直分享業界流行技術方案,讓我們共同學習共同進步。
下一篇預告:聊聊我們緩存中的高可用話題
往期精選
你一定要掌握這種緩存讀寫策略,開發必備
消息中間件能干什么?RabbitMQ、Kafka、RocketMQ正確選型姿勢
NoSql數據庫,是怎么解決我們高并發場景下MySql表現的不足
數據庫分庫分表,手把手教你怎么去動態擴容索容
每天百萬交易的支付系統,生產環境該怎么設置JVM堆內存大小
你的成神之路我已替你鋪好,沒鋪你來捶我
總結
以上是生活随笔為你收集整理的先来先服务算法代码_一致性哈希算法编写的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python 通登录银行_Python3
- 下一篇: next数组_【阿里面试热身题】数组去重