一致性Hash算法原理(手寫Hash算法,通俗易懂)


一致性Hash算法應用場景
請求的負載均衡:比如Nginx的ip_hash策略,通過對IP的Hash值來額定將請求轉發到哪台Tomcat
分布式存儲:比如分布式集群架構Redis、Hadoop、ElasticSearch、Mysql分庫分表,數據存入哪台服務器,就可以通過Hash算法來確定
普通Hash算法存在的問題
如果服務器減少或者增多,所有客戶端都需要進行重新Hash,然后進行分配。
一致性Hash算法
一條直線上有0到2^32-1,這些正整數。然后我們將其頭尾相接,變成一個圓環形成閉環(Hash環)
我們以Nginx的ip_hash分發請求為例說明一致性Hash算法:
對服務器的IP進行hash算法,獲取Hash值,然后放到Hash閉環對應位置上;
通過對客戶端的IP進行Hash算法獲取Hash值,然后順時針查找距離該Hash最近的服務器,並將請求轉發到該服務器。
這樣的話服務器進行擴展或者縮減后,比如縮減了一台服務器3,那么服務器3和服務器2之間的請求會被轉發到服務器4上,而其他的請求不會受到影響,這就是一致性Hash的好處,將影響程度減小。
PS:這種方式有一個問題
假如就兩台服務器,通過Hash算法后位置如下:
服務器1-服務器2之間的客戶端請求都轉發到了服務器2上,然而其他大量請求都轉發到了服務器1上,這就導致服務器1壓力過大,這種問題叫做數據傾斜問題。
數據傾斜問題解決
一致性Hash算法引入了虛擬節點機制
具體做法是在服務器IP或者主機名后面增加編號來實現。比如可以為每台服務器計算是哪個虛擬節點,於是可以分別計算“服務器1IP#1,服務器2IP#2,服務器1IP#3,服務器2IP#1,服務器2IP#2,服務器2IP#3”的哈希值,於是形成了6個虛擬節點,如下圖
這樣的話客戶端經過Hash算法后就會被更加均勻的分布在Hash閉環上,請求也會被更加均勻的分發在各個服務器上,從而解決了數據傾斜問題
普通Hash算法代碼實現
 
package com.lagou;

/**
 * @ClassName: GeneralHash
 * @Description: 普通Hash算法
 * @Author: qjc
 * @Date: 2021/9/7 6:25 下午
 */
public class GeneralHash {

    public static void main(String[] args) {
        // 客戶端IP集合
        String[] clientList = new String[]{"127.1.1.1", "127.1.1.2", "127.1.1.3"};

        // 服務器數量
        int serverCount = 5;
        // 通過Hash算法,獲取hash值,路由到服務器。hash(ip)%serverCount
        for (String clientIP : clientList) {
            int index = Math.abs(clientIP.hashCode()) % serverCount;
            System.err.println("客戶端:" + clientIP + ",對應服務器:" + index);
        }
    }
}

結果如下

 

 這樣有個問題,如果服務器減少或者增加,對請求的轉發影響都會特別大

一致性Hash算法代碼實現

package com.lagou;

import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @ClassName: ConsistentHash
 * @Description: 一致性Hash算法
 * @Author: qjc
 * @Date: 2021/9/7 6:32 下午
 */
public class ConsistentHash {

    public static void main(String[] args) {
        // 服務端IP集合
        String[] serverList = new String[]{"127.0.0.1", "127.0.0.4", "127.0.0.7"};
        // 然后將服務端IP進行Hash然后存儲到map中,想象成一個閉環
        SortedMap<Integer, String> serverHashMap = new TreeMap<>();
        for (int i = 0; i < serverList.length; i++) {
            String serverIP = serverList[i];
            // Hash值我們用IP的最后一位代替,這是為了測試
            int serverHash = Integer.valueOf(serverIP.split("\\.")[3]);
            serverHashMap.put(serverHash, serverIP);
        }
        // 客戶端IP集合
        String[] clientList = new String[]{"127.1.1.1", "127.1.1.2", "127.1.1.3", "127.1.1.4", "127.1.1.5", "127.1.1.6", "127.1.1.7", "127.1.1.8", "127.1.2.9"};

        for (int i = 0; i < clientList.length; i++) {
            String clientIP = clientList[i];
            int clientHash = Integer.valueOf(clientIP.split("\\.")[3]);
            // tailMap方法獲取的是參數key(包含key)后面的map,比如
            // serverIPMap = {1="127.0.0.1",2="127.0.0.2",3="127.0.0.3",4="127.0.0.4"}
            // serverIPMap.tailMap(2) = {2="127.0.0.2",3="127.0.0.3",4="127.0.0.4"}
            SortedMap<Integer, String> integerStringSortedMap = serverHashMap.tailMap(clientHash);
            if (integerStringSortedMap.isEmpty()) {
                // 如果為空,則只能轉發到第一個服務器上
                Integer firstServerIP = serverHashMap.firstKey();
                System.err.println("客戶端:" + clientIP + ",服務器:" + serverHashMap.get(firstServerIP));
            } else {
                // 如果不為空,則轉發到最近的服務器IP
                Integer nearestServerIP = integerStringSortedMap.firstKey();
                System.err.println("客戶端:" + clientIP + ",服務器:" + serverHashMap.get(nearestServerIP));
            }
        }
    }
}

結果如下

這種情況下如果服務器減少或者增加,對請求的分發影響不大,但是有個問題,服務器如果不是均勻分布在Hash閉環上,就會導致數據傾斜

一致性Hash算法(含虛擬節點)

 

package com.lagou;

import java.util.SortedMap;
import java.util.TreeMap;

/**
 * @ClassName: ConsistenHashWithVirtual
 * @Description: 使用虛擬節點的一致性Hash算法
 * @Author: Administrator
 * @Date: 2021/9/7 21:41
 */
public class ConsistenHashWithVirtual {

    public static void main(String[] args) {
        // 服務端IP集合
        String[] serverList = new String[]{"127.0.0.1", "127.0.0.4", "127.0.0.7"};
        // 然后將服務端IP進行Hash然后存儲到map中,想象成一個閉環
        SortedMap<Integer, String> serverHashMap = new TreeMap<>();

        int virtualCount = 2;

        for (int i = 0; i < serverList.length; i++) {
            String serverIP = serverList[i];
            // Hash值我們用IP的最后一位代替,這是為了測試
            int serverHash = Integer.valueOf(serverIP.split("\\.")[3]);
            serverHashMap.put(serverHash, serverIP);
            for (int j = 0; j < virtualCount; j++) {
                // 虛擬節點Hash值
                int virtualServerHash = Integer.valueOf(serverIP.split("\\.")[3]) + (j + 1);
                serverHashMap.put(virtualServerHash, "請求轉發到虛擬節點:" + serverIP + "(虛擬后綴:" + (j + 1) + ")");
            }
        }
        // 客戶端IP集合
        String[] clientList = new String[]{"127.1.1.1", "127.1.1.2", "127.1.1.3", "127.1.1.4", "127.1.1.5", "127.1.1.6", "127.1.1.7", "127.1.1.8", "127.1.2.9"};

        for (int i = 0; i < clientList.length; i++) {
            String clientIP = clientList[i];
            int clientHash = Integer.valueOf(clientIP.split("\\.")[3]);
            // tailMap方法獲取的是參數key(包含key)后面的map,比如
            // serverIPMap = {1="127.0.0.1",2="127.0.0.2",3="127.0.0.3",4="127.0.0.4"}
            // serverIPMap.tailMap(2) = {2="127.0.0.2",3="127.0.0.3",4="127.0.0.4"}
            SortedMap<Integer, String> integerStringSortedMap = serverHashMap.tailMap(clientHash);
            if (integerStringSortedMap.isEmpty()) {
                // 如果為空,則只能轉發到第一個服務器上
                Integer firstServerIP = serverHashMap.firstKey();
                System.err.println("客戶端:" + clientIP + ",服務器:" + serverHashMap.get(firstServerIP));
            } else {
                // 如果不為空,則轉發到最近的服務器IP
                Integer nearestServerIP = integerStringSortedMap.firstKey();
                System.err.println("客戶端:" + clientIP + ",服務器:" + serverHashMap.get(nearestServerIP));
            }

        }
    }

}

 

運行結果如下

 這樣就大大減少了服務器壓力


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM