實戰:緩存路由(一致性Hash)算法Java版實現?

負載均衡之緩存路由(一致性Hash)算法Java實現

  分佈式系統中負載均衡的問題時候可使用Hash算法讓固定的一部分請求落到同一臺服務器上,這樣每臺服務器固定處理一部分請求(並維護這些請求的信息),起到負載均衡的做用。好比說分佈式緩存,既然是緩存,就沒有必要去作一個全部機器上的數據都徹底同樣的緩存集羣,而是應該設計一套好的緩存路由工具類,因此一致性Hash算法就所以而誕生了。java

  衡量一個一致性Hash算法最重要的兩個特徵:node

平衡性:平衡性是指哈希的結果可以儘量分佈到全部的緩衝中去,這樣可使得全部的緩衝空間都獲得利用。面試

單調性:單調性是指若是已經有一些數據經過哈希分配到了相應的機器上,又有新的機器加入到系統中。哈希的結果應可以保證原有的數據要麼仍是呆在它所在的機器上不動,要麼被遷移到新的機器上,而不會遷移到舊的其餘機器上。算法

  業界經常使用的兩種一致性Hash算法,一種是不帶虛擬節點的Hash算法,另一種是帶虛擬節點的Hash算法。apache


  下面的代碼就是不帶虛擬節點的一致性Hash算法,原理稍後分析:緩存

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;

/**
 * 一致性Hash算法
 * */
public class ConsistentHashWithoutVirtualNode {
    /**
     * 服務器節點信息
     * */
    private static SortedMap<Integer, String> nodeMap = new TreeMap<>();
    //服務器配置信息(可配置)
    private static String[] servers = {"192.168.56.120:6379",
                                       "192.168.56.121:6379", 
                                       "192.168.56.122:6379",
                                       "192.168.56.123:6379", 
                                       "192.168.56.124:6379"};
    
    /**
     * 初始化
     * */
    static{
        for(int i=0; i< servers.length; i++){
            nodeMap.put(getHash(servers[i]), servers[i]);
        }
        System.out.println("Hash環初始化完成!");
    }
    
     /**
     * 經典的Time33 hash算法
     * */
    public static int getHash(String key) {
        if(StringUtils.isEmpty(key)) 
            return 0;
        try{
            MessageDigest digest = MessageDigest.getInstance("MD5");
            key = new String(digest.digest(key.getBytes()));
        }catch(NoSuchAlgorithmException e){
            e.printStackTrace();
        }
        int hash = 5381;
        for (int i = 0; i < key.length(); i++) {
            int cc = key.charAt(i);
            hash += (hash << 5) + cc;
        }
        return hash<0 ? -hash : hash;
    }
    
    /**
     * 緩存路由算法
     * */
    public static String getServer(String key){
        int hash = getHash(key);
        //獲得大於該Hash值的全部Map
        SortedMap<Integer, String> subMap = nodeMap.tailMap(hash);
        if(subMap.isEmpty()){
            int index = nodeMap.firstKey();
            System.out.printf("%s被路由到節點[%s]\n", key, nodeMap.get(index));
            return nodeMap.get(index);
        }else{
            int index = subMap.firstKey();
            System.out.printf("%s被路由到節點[%s]\n", key, nodeMap.get(index));
            return nodeMap.get(index);
        }      
    }
    
    /**
     * 使用UUID模擬隨機key
     * */
    public static void main(String[] args) {
        for(int i=0; i<20; i++){
            String str = UUID.randomUUID().toString();
            getServer(str);
        }
    }  
}
複製代碼

  首先,針對平衡性,咱們須要選擇一個好的Hash函數,咱們選擇的Hash算法是業界內比較出名的Time33 Hash算法,這個大家能夠百度一下。可是Time33 Hash算法有一個弊端,那就是對於兩個key差很少的字符串來講,他們生成的Hash值很接近,因此咱們的解決辦法就是在生成Hash值以前先用MD5算法取一次信息指紋。bash

  nodeMap是用來保存服務器節點信息的SortedMap(key是hash值,value是服務器節點信息); servers是服務器的配置信息;使用static靜態代碼塊初始化nodeMap保存節點信息。服務器

  緩存路由算法是核心代碼,大概思想是先計算key的hash值,而後用hash值找到nodeMap中的全部鍵值大於該hash值的鍵值對,若是找到,取鍵值對最小的那個鍵值對的值做爲路由結果;若是沒有找到鍵值對的鍵大於該hash值的鍵值對,那麼就取nodeMap裏鍵值對的鍵最小的那個值做爲路由結果。微信

  接下來,咱們使用UUID生成隨機字符串測試一下吧,測試結果以下:架構

TIM圖片20190610195606.png
  可是這種不帶虛擬節點的路由算法有個問題,在增減機器時會使舊的數據大量「失效」,也稱爲 命中率降低

  因而咱們選擇把一個機器分爲不少個虛擬節點,而且使這些虛擬節點交叉的分散在一個hash環上,通過改良後的代碼以下:

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import org.apache.commons.lang.StringUtils;

public class ConsistentHashWithVirtualNode {
    /**
     * 虛擬節點信息
     * key:hash值
     * value:真實節點+"&"+序號
     * */
    private static SortedMap<Integer, String> virtualNodeMap = new TreeMap<>();
    //單機虛擬節點
    private static final int VIRTUAL_NODE_NUM = 5;
    //服務器配置信息(可配置)
    private static String[] servers = {"192.168.56.120:6379",
                                       "192.168.56.121:6379", 
                                       "192.168.56.122:6379",
                                       "192.168.56.123:6379", 
                                       "192.168.56.124:6379"};
    
    /**
     * 初始化
     * */
    static{
        for(int i=0; i< servers.length; i++){
            for(int j=0; j<VIRTUAL_NODE_NUM; j++){
                String virtualNodeName = servers[i] + "&" + j;
                virtualNodeMap.put(getHash(virtualNodeName), virtualNodeName);
            }
        }
        System.out.println("帶虛擬節點的Hash環初始化完成!");
        
    }
    
     /**
     * 經典的Time33 hash算法
     * */
    public static int getHash(String key) {
        if(StringUtils.isEmpty(key)) 
            return 0;
        try{
            MessageDigest digest = MessageDigest.getInstance("MD5");
            key = new String(digest.digest(key.getBytes()));
        }catch(NoSuchAlgorithmException e){
            e.printStackTrace();
        }
        int hash = 5381;
        for (int i = 0; i < key.length(); i++) {
            int cc = key.charAt(i);
            hash += (hash << 5) + cc;
        }
        return hash<0 ? -hash : hash;
    }
    
    /**
     * 緩存路由算法
     * */
    public static String getServer(String key){
        int hash = getHash(key);
        //獲得大於該Hash值的全部Map
        SortedMap<Integer, String> subMap = virtualNodeMap.tailMap(hash);
        if(subMap.isEmpty()){
            int index = virtualNodeMap.firstKey();
            System.out.printf("%s被路由到虛擬節點[%s]真實節點[%s]\n", key, virtualNodeMap.get(index), 
                    virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&")));
            return virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&"));
        }else{
            int index = subMap.firstKey();
            System.out.printf("%s被路由到虛擬節點[%s]真實節點[%s]\n", key, virtualNodeMap.get(index), 
                    virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&")));
            return virtualNodeMap.get(index).substring(0, virtualNodeMap.get(index).indexOf("&"));
        }      
    }
    
    /**
     * 使用UUID模擬隨機key
     * */
    public static void main(String[] args) {
        for(int i=0; i<20; i++){
            String str = UUID.randomUUID().toString();
            getServer(str);
        }
    }
    
}
複製代碼

  虛擬節點的大體思想之這樣的,使用真實節點+"&"+序號(序號的範圍是0到單臺服務器所需的虛擬節點個數VIRTUAL_NODE_NUM)做爲虛擬節點的值,核心代碼如上面截圖所示。

  接下來,咱們仍是使用UUID生成隨機字符串測試一下吧,測試結果以下:

TIM圖片20190610195606.png

  好的,大功告成,以上就是關於一致性Hash路由算法的所有內容。PS:以上代碼有刪減,僅提取了其中的核心代碼。若是喜歡個人內容的話,歡迎轉發,謝謝。

  歡迎你們關注個人微信公衆號"Java架構師養成記",不按期分享各種面試題、採坑經歷。

Java架構師養成記.jpg
相關文章
相關標籤/搜索