隨着業務系統愈來愈大,咱們須要對API的訪問進行更多的緩存,使用Redis是一個很好的解決方案.java
可是單臺Redis性能不足夠且早晚要走向集羣的,那麼怎麼才能良好的利用Redis集羣來進行緩存呢?redis
當一個請求到來,咱們如何決定將這個請求的內容緩存在那臺Redis服務器上?咱們一一道來.算法
隨機分配緩存
假設咱們有X臺服務器,當一個請求來到的時候,咱們獲取一個0-X
的隨機數,而後將內容緩存在該服務器上.安全
這明顯是不可選的,想要查詢的時候咱們本身也不知道在哪,只能逐個遍歷服務器,知道拿到爲止.服務器
hash取模數據結構
還有一種常見的方式就是對集羣數量
進行hash取模.好比咱們如今有3臺服務器,那麼對請求的key進行hash,以後拿到的hashcode
對3進行取模,獲得的數字就是該key應該存儲的服務器.併發
這樣雖然解決了上面的獲取問題,可是擴展性極其差,設想一下如今咱們須要新添加一臺機器,也就是機器數量來到了4
,那麼對4取模的結果和對3
取模的結果基本上所有不同,也就是說咱們須要對全部的key進行一次從新的hash計算並從新存儲.負載均衡
一致性hash分佈式
這也是咱們今天的重點,它於1997年由麻省理工學院提出.咱們在下面單獨講解一下他.
其實本質上,一致性hash也是hash取模,只是是永遠的對2的32次方-1
取模.
一致性hash引入了一個叫作一致性hash環
的概念,即將(0-2^32-1)
中間的全部整數首尾相接鏈接成一個環.以下圖:
而後將全部的節點映射到環上,假設咱們有3個節點,N1,N2.N3.那麼以下圖:
以後咱們將要存儲的全部key也都映射到環上,假設咱們有6個key.
這樣以後,順時針旋轉key,將其存儲在遇到的第一個服務器上,這樣有什麼好處呢?
那就是擴展性,當新插入一個節點時,只會影響到少部分key,須要從新計算的key不多,咱們添加一個節點試試:
能夠發現,只有N3數據須要從N2節點遷移到N4.
是否是看起來挺美滋滋的,啥好處都有,有啥缺點呢?
缺點固然有.
上面的圖是一種理想狀態,基本算是均勻的分佈了,可是實際使用中,你用一個集羣中的機器名(有很大的可能性很相似)去hash,拿到的結果可能很相近,也就是說,並非像圖中這樣分散的,而是彙集在一塊兒,而key是分散的,這樣會致使,大量的key命中了其中一個或者多個服務器,而有一部分卻空閒.總之,負載不均衡.
redis的key都是字符串,而字符串的hashcode
方法是可能會返回負值的,而一致性hash環是隻有正值的,所以須要咱們使用別的hash算法.(淡然你也能夠粗暴的進行取絕對值).
hash不均勻主要出如今節點不多的時候,那麼咱們能夠手動模擬一些節點出來,也就是所謂的虛擬節點,好比咱們只有3個節點,可是咱們定義一個規則,好比A-1,A-2,A-3,這三個節點均可以被映射到環上,可是在真正存儲的時候咱們都存儲在A上.
只要咱們的虛擬節點足夠多,咱們就可讓其儘量的均勻分佈在環上.
一致性hash算法是使用虛擬的環狀數據結構,解決了簡單hash算法中擴展性差的問題,在分佈式緩存以及負載均衡中有許多的應用.
Java中提供了ConcurrentSkipListMap
類,能夠很好的使用在這裏,不只能夠輕鬆的模擬環狀結構,併發安全且使用跳錶結構的ConcurrentSkipListMap
能夠提供很好的併發性能.
對於虛擬節點的多少,實際上是能夠大概估算出來的,所以在下面的代碼中,我將其做爲一個變量,在初始化的時候由當前節點的數量計算獲得,固然我沒有具體實現計算方法.這麼設計是出於什麼考慮呢,想讓虛擬節點的數量儘可能的剛恰好,萬一節點不少,仍是用固定的虛擬節點,對均勻性提高不會很大,反而會形成性能損耗等.
代碼中主要提供了一下幾個方法:
好了,廢話很少說了,都在註釋裏面了!
package util;
import redis.clients.jedis.Jedis;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
/** * Created by pfliu on 2019/05/19. */
public class ConsistentHashRedis {
// 用跳錶模擬一致性hash環,即便在節點不少的狀況下,也能夠有不錯的性能
private final ConcurrentSkipListMap<Integer, String> circle;
// 虛擬節點數量
private final int virtual_size;
public ConsistentHashRedis(String configs) {
this.circle = new ConcurrentSkipListMap<>();
String[] cs = configs.split(",");
this.virtual_size = getVirtualSize(cs.length);
for (String c : cs) {
this.add(c);
}
}
/** * 將每一個節點添加進環中,而且添加對應數量的虛擬節點 */
private void add(String c) {
if (c == null) return;
for (int i = 0; i < virtual_size; ++i) {
String virtual = c + "-N" + i;
int hash = getHash(virtual);
circle.put(hash, virtual);
}
}
// 根據字符串獲取hash值,這裏使用簡單粗暴的絕對值.
private int getHash(String s) {
return Math.abs(s.hashCode());
}
// 計算當前須要多少個虛擬節點,這裏沒有計算,直接使用了150.
private int getVirtualSize(int length) {
return 150;
}
/** * 對外提供的set方法 */
public void set(String key, String v) {
getJedisFromCircle(key).set(key, v);
}
public String get(String k) {
return getJedisFromCircle(k).get(k);
}
/** * 從環中取到適合當前key的jedis. */
private Jedis getJedisFromCircle(String key) {
int keyHash = getHash(key);
ConcurrentNavigableMap<Integer, String> tailMap = circle.tailMap(keyHash);
String config = tailMap.isEmpty() ? circle.firstEntry().getValue() : tailMap.firstEntry().getValue();
// 注意,因爲使用了虛擬節點,因此這裏要作 虛擬節點 -> 真實節點的映射
String[] cs = config.split("-");
return new Jedis(cs[0]);
}
/** * 對外暴露的添加節點接口 */
public boolean addJedis(String cs) {
add(cs);
return true;
}
/** * 對外暴露的刪除節點節點 */
public boolean deleteJedis(String cs) {
delete(cs);
return true;
}
/** * 從環中刪除一個節點極其虛擬節點 */
private void delete(String cs) {
if (cs == null) return;
for (int i = 0; i < virtual_size; ++i) {
String virtual = cs + "-N" + i;
int hash = getHash(virtual);
circle.remove(hash, virtual);
}
}
}
複製代碼
歡迎轉載,煩請署名並保留原文連接。
聯繫郵箱:huyanshi2580@gmail.com
更多學習筆記見我的博客------>呼延十