一致性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)); } } } }
運行結果如下
這樣就大大減少了服務器壓力