redis一致性Hash,
在微服務領域,使用Redis做緩存可并不是一件容易的事情。
像新浪、推特這樣的應用,許許多多的熱點數據全都存放在Redis這一層,打到DB層的請求并不多,可以說非常依賴緩存了。如果緩存掛掉,流量全部穿透到DB層,其必然不堪其重,整個系統也會隨之癱瘓,后果非常嚴重。
由于緩存數據量很大,Redis快正是快在其基于內存的快速存取,而計算機的內存資源又是十分有限的,故分布式緩存集群面臨著伸縮性的要求。
問題就在這時出現了,所有的緩存數據是分散存放在各個Redis節點上的,通過客戶端實現路由算法,來將某個key路由到某個具體的節點。
這個路由算法是分布式緩存伸縮性能否成功的關鍵。
它的職責不僅僅是由key算出一個Redis的地址,而且必須讓新上線的緩存服務器對整個分布式緩存集群影響最小,使得擴容后,整個緩存服務器集群中已經緩存的數據盡可能還被訪問到。
這里可以舉一個例子,比如用取余數(hash(key)%serverNum)做為該算法,Redis需要由3個節點,擴大到4個節點,會有75%的key無法命中,如下圖:
| hash(key) | hash(key)/3 | hash(key)/4 | 是否命中 |
|---|---|---|---|
| 1 | 1 | 1 | 是 |
| 2 | 2 | 2 | 是 |
| 3 | 0 | 3 | 否 |
| 4 | 1 | 0 | 否 |
| 5 | 2 | 1 | 否 |
| 6 | 0 | 2 | 否 |
| 7 | 1 | 3 | 否 |
| 8 | 2 | 0 | 否 |
| 9 | 0 | 1 | 否 |
| 10 | 1 | 2 | 否 |
| 11 | 2 | 3 | 否 |
| 12 | 0 | 0 | 是 |
這種效果非常糟糕,當服務器數量為100臺時,再增加一臺新服務器,不能命中率將達到99%,這和整個緩存服務掛了一個效果。
而一致性Hash正是為了解決這個問題而出現的,該路由算法通過引入一個一致性Hash環,以及進一步增加虛擬節點層,來實現盡可能高的命中率。
關于該算法的具體原理與網上已經有一些說得很透徹的文章,本文不再贅述。
本機部署多個Redis節點
要對一致性Hash進行驗證,要做好準備工作,最直接地,首先要有一個Redis集群。這里我通過使用在本機上部署多個Redis實例指向不同端口來模擬這一形態。
建立項目目錄:$ mkdir redis-conf
之后將redis的配置copy一份過來并復制為5份,分別命名為redis-6379.conf~redis-6383.conf。
需要對其內容進行一些修改才能正常啟動,分別找到配置文件中的如下兩行并對數字進行相應修改。
port 6379
pidfile /var/run/redis_6379.pid
然后就可以分別啟動了:redis-server ./redis-6379 &
可以使用redis-cli -p 6379來指定連接的redis-server。
不妨進行一次嘗試,比如在6379設置key 1 2,而到6380 get 1只能得到nil,說明它們是各自工作的,已經滿足可以測試的條件。

代碼實現
先說一下思路。
部署4個節點,從6379到6382,通過一致性Hash算法,將key: 0~99999共100000個key分別set到這4個服務器上,然后再部署一個節點6383,這時再從0到99999開始get一遍,統計get到的次數來驗證命中率是否為期望的80%(4/5)。
一致性Hash算法的實現嚴重借鑒了這篇文章,使用紅黑樹來做數據結構,來實現log(n)的查找時間復雜度,使用FNV1_32_HASH哈希算法來盡可能使key與節點分布得更加均勻,引入了虛擬節點,來做負載均衡。
建議讀者詳細看下這篇文章,里面的講解非常詳細易懂。
下面是我改寫過后的代碼:
package org.guerbai.io.jedistry;
import redis.clients.jedis.Jedis;
import java.util.*;
class JedisProxy {
private static String[][] redisNodeList = {
{"localhost", "6379"},
{"localhost", "6380"},
{"localhost", "6381"},
{"localhost", "6382"},
};
private static Map<String, Jedis> serverConnectMap = new HashMap<>();
private static SortedMap<Integer, String> virtualNodes = new TreeMap<>();
private static final int VIRTUAL_NODES = 100;
static
{
for (String[] str: redisNodeList)
{
addServer(str[0], str[1]);
}
System.out.println();
}
private static int getHash(String str)
{
final int p = 16777619;
int hash = (int)2166136261L;
for (int i = 0; i < str.length(); i++)
hash = (hash ^ str.charAt(i)) * p;
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
// 如果算出來的值為負數則取其絕對值
if (hash < 0)
hash = Math.abs(hash);
return hash;
}
private static String getServer(String node)
{
// 得到帶路由的結點的Hash值
int hash = getHash(node);
// 得到大于該Hash值的所有Map
SortedMap<Integer, String> subMap =
virtualNodes.tailMap(hash);
// 第一個Key就是順時針過去離node最近的那個結點
if (subMap.
