本文是接上篇文章以後的第二篇,主要是一致性hash算法的實現,下面會經過詳細註解來描述流程。有不清楚的能夠評論留言。java
首先咱們來看下ConsistentHashLoadBalance的核心結構,先有個整體的概念,而後順着方法調用流程逐步往下講。node
public class ConsistentHashLoadBalance extends AbstractLoadBalance {
public static final String NAME = "consistenthash";
//一個方法對應一個一致性hash選擇器
private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();
@SuppressWarnings("unchecked")
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
// 精確到方法,最爲緩存一致性hash選擇器的key
String methodName = RpcUtils.getMethodName(invocation);
String key = invokers.get(0).getUrl().getServiceKey() + "." + methodName;
int identityHashCode = System.identityHashCode(invokers); //基於invoker集合,根據對象內存地址來定義hash值
//得到ConsistentHashSelector,爲空則建立再緩存(要麼第一次,要麼invokers集合發生了變化,都須要從新建立ConsistentHashSelector)
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.identityHashCode != identityHashCode) {
//初始化並緩存一致性hash選擇器
selectors.put(key, new ConsistentHashSelector<T>(invokers, methodName, identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation); //選擇一個invoker
}
//接下來要講下一致性hash選擇器了
private static final class ConsistentHashSelector<T> {
//.....
}
}複製代碼
上面的doSelect方法主要的做用就是建立和初始化了一致性hash選擇器。下面咱們看下這個選擇器是如何實現負載均衡的。算法
private static final class ConsistentHashSelector<T> {
//treemap底層是棵紅黑樹。
//紅黑樹不瞭解的,能夠看下個人博客 https://juejin.im/post/5bef5de46fb9a049c15ecbd9
private final TreeMap<Long, Invoker<T>> virtualInvokers;
private final int replicaNumber; //每一個invoker對應的虛擬節點數
private final int identityHashCode; //定義hash值
private final int[] argumentIndex; //參數位置數組(缺省是第一個參數)
ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
this.virtualInvokers = new TreeMap<Long, Invoker<T>>(); // 虛擬節點與 Invoker 的映射關係
this.identityHashCode = identityHashCode;
URL url = invokers.get(0).getUrl();
//缺省用 160 份虛擬節點,若是要修改,請配置 <dubbo:parameter key="hash.nodes" value="320" />
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
//缺省只對第一個參數 Hash,若是要修改,請配置 <dubbo:parameter key="hash.arguments" value="0,1" />
String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
argumentIndex = new int[index.length];
for (int i = 0; i < index.length; i++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
//初始化virtualInvokers,這樣能夠最終每一個invoker都有replicaNumber個虛擬節點
for (Invoker<T> invoker : invokers) {
String address = invoker.getUrl().getAddress();
//每4個虛擬節點爲一組,4個節點一共32位。之因此是32位是由於一致性哈希環取值範圍爲0~2^32
for (int i = 0; i < replicaNumber / 4; i++) {
// 地址加上後綴數字,計算md5值
byte[] digest = md5(address + i);
// Md5是一個16字節長度的數組,將16字節的數組每四個字節一組,分別對應一個虛擬結點
for (int h = 0; h < 4; h++) {
//計算hash值,做爲key,存儲節點
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
public Invoker<T> select(Invocation invocation) {
String key = toKey(invocation.getArguments()); //參數轉換爲string類型
byte[] digest = md5(key); // key md5計算,返回16個字節的byte數組
// 一、hash(digest, 0),取前四個字節,計算hash值
// 二、查找臨近節點,獲取invoker
return selectForKey(hash(digest, 0));
}
//這個方法太簡單
private String toKey(Object[] args) {
StringBuilder buf = new StringBuilder();
for (int i : argumentIndex) {
if (i >= 0 && i < args.length) {
buf.append(args[i]);
}
}
return buf.toString();
}
private Invoker<T> selectForKey(long hash) {
//返回大於或等於給定鍵,若是不存在這樣的鍵的鍵 - 值映射,則返回null相關聯(treemap底層是棵紅黑樹,紅黑樹不瞭解的,能夠看下個人博客 https://juejin.im/post/5bef5de46fb9a049c15ecbd9 )
Map.Entry<Long, Invoker<T>> entry = virtualInvokers.ceilingEntry(hash);
if (entry == null) {
//若是不存在,那麼可能這個hash值比虛擬節點的最大值還大,那麼取第一個。這樣就造成了一個環
entry = virtualInvokers.firstEntry();
}
return entry.getValue();
}
private long hash(byte[] digest, int number) {
return (((long) (digest[3 + number * 4] & 0xFF) << 24)
| ((long) (digest[2 + number * 4] & 0xFF) << 16)
| ((long) (digest[1 + number * 4] & 0xFF) << 8)
| (digest[number * 4] & 0xFF))
& 0xFFFFFFFFL;
}
private byte[] md5(String value) {
MessageDigest md5;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException(e.getMessage(), e);
}
md5.reset();
byte[] bytes;
try {
bytes = value.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e.getMessage(), e);
}
md5.update(bytes);
return md5.digest();
}
}複製代碼
其實ConsistentHashLoadBalance就是實現了一致性hash算法,以前瞭解這種算法的同窗可能很輕鬆就能看懂。dubbo的實現其實也很簡單,步驟總結以下:數組
一、生成虛擬節點:每一個invoker生成replicaNumber個hash值,即產生replicaNumber個虛擬節點。緩存
二、請求者生成hash值:請求方法進來,會按配置的值,取指定的參數,拼接字符串,生成md5值,再取數組的前4個值生成hash值。bash
三、取臨近節點:根據步驟2生成的hash值,取treemap中獲取大於等於該hash值的節點。若是節點不存在,則取第一個節點(造成環)。app
四、返回invoker 負載均衡