本篇主要對dubbo集群容錯進(jìn)行剖析涉波,主要下面幾個模塊
- cluster容錯方案
- Directory目錄服務(wù)
- route 路由解析
- loadBalance 軟負(fù)載均衡
一、調(diào)用鏈路
二、容錯方案
集群模式的配置
<dubbo:service cluster="failsafe" /> 服務(wù)提供方
<dubbo:reference cluster="failsafe" /> 服務(wù)消費(fèi)方
集群容錯實(shí)現(xiàn)
接口類 com.alibaba.dubbo.rpc.cluster.Cluster
1.AvailableCluster
獲取可用的調(diào)用贤姆。遍歷所有Invokers判斷Invoker.isAvalible,只要一個有為true直接調(diào)用返回沐飘,不管成不成功
2.BroadcastCluster
廣播調(diào)用。遍歷所有Invokers, 逐個調(diào)用每個調(diào)用catch住異常不影響其他invoker調(diào)用
3.FailbackCluster
失敗自動恢復(fù)桩皿, 對于invoker調(diào)用失敗, 后臺記錄失敗請求扳埂,任務(wù)定時重發(fā), 通常用于通知
//FailbackClusterInvoker
//記錄失敗的調(diào)用
private final ConcurrentMap<Invocation, AbstractClusterInvoker<?>> failed = new ConcurrentHashMap<Invocation, AbstractClusterInvoker<?>>();
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
checkInvokers(invokers, invocation);
Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
return invoker.invoke(invocation);
} catch (Throwable e) {
//失敗后調(diào)用 addFailed
addFailed(invocation, this);
return new RpcResult(); // ignore
}
}
private void addFailed(Invocation invocation, AbstractClusterInvoker<?> router) {
if (retryFuture == null) {
synchronized (this) {
if (retryFuture == null) {
retryFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
public void run() {
// 收集統(tǒng)計(jì)信息
try {
retryFailed();
} catch (Throwable t) { // 防御性容錯
logger.error("Unexpected error occur at collect statistic", t);
}
}
}, RETRY_FAILED_PERIOD, RETRY_FAILED_PERIOD, TimeUnit.MILLISECONDS);
}
}
}
failed.put(invocation, router);
}
//失敗的進(jìn)行重試业簿,重試成功后移除當(dāng)前map
void retryFailed() {
if (failed.size() == 0) {
return;
}
for (Map.Entry<Invocation, AbstractClusterInvoker<?>> entry : new HashMap<Invocation, AbstractClusterInvoker<?>>(
failed).entrySet()) {
Invocation invocation = entry.getKey();
Invoker<?> invoker = entry.getValue();
try {
invoker.invoke(invocation);
failed.remove(invocation);
} catch (Throwable e) {
logger.error("Failed retry to invoke method " + invocation.getMethodName() + ", waiting again.", e);
}
}
}
4.FailfastCluster
快速失敗,只發(fā)起一次調(diào)用阳懂,失敗立即保錯梅尤,通常用于非冪等性操作
5.FailoverCluster default
失敗轉(zhuǎn)移,當(dāng)出現(xiàn)失敗岩调,重試其它服務(wù)器巷燥,通常用于讀操作,但重試會帶來更長延遲
(1) 目錄服務(wù)directory.list(invocation) 列出方法的所有可調(diào)用服務(wù)
獲取重試次數(shù)号枕,默認(rèn)重試兩次
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
(2) 根據(jù)LoadBalance負(fù)載策略選擇一個Invoker
(3) 執(zhí)行invoker.invoke(invocation)調(diào)用
(4) 調(diào)用成功返回
調(diào)用失敗小于重試次數(shù)缰揪,重新執(zhí)行從3)步驟開始執(zhí)行,調(diào)用次數(shù)大于等于重試次數(shù)拋出調(diào)用失敗異常
6.FailsafeCluster
失敗安全葱淳,出現(xiàn)異常時钝腺,直接忽略,通常用于寫入審計(jì)日志等操作赞厕。
7.ForkingCluster
并行調(diào)用艳狐,只要一個成功即返回,通常用于實(shí)時性要求較高的操作皿桑,但需要浪費(fèi)更多服務(wù)資源毫目。
注:
還有 MergeableCluster 和 MockClusterWrapper策略,但是個人沒有用過所以就不說了
三诲侮、Directory目錄服務(wù)
1. StaticDirectory
靜態(tài)目錄服務(wù)镀虐, 它的所有Invoker通過構(gòu)造函數(shù)傳入, 服務(wù)消費(fèi)方引用服務(wù)的時候沟绪, 服務(wù)對多注冊中心的引用刮便,將Invokers集合直接傳入 StaticDirectory構(gòu)造器
public StaticDirectory(URL url, List<Invoker<T>> invokers, List<Router> routers) {
super(url == null && invokers != null && invokers.size() > 0 ? invokers.get(0).getUrl() : url, routers);
if (invokers == null || invokers.size() == 0)
throw new IllegalArgumentException("invokers == null");
this.invokers = invokers;
}
StaticDirectory的list方法直接返回所有invoker集合
@Override
protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
return invokers;
}
2. RegistryDirectory
注冊目錄服務(wù), 它的Invoker集合是從注冊中心獲取的绽慈, 它實(shí)現(xiàn)了NotifyListener接口實(shí)現(xiàn)了回調(diào)接口notify(List<Url>)诺核。
比如消費(fèi)方要調(diào)用某遠(yuǎn)程服務(wù)抄肖,會向注冊中心訂閱這個服務(wù)的所有服務(wù)提供方,訂閱時和服務(wù)提供方數(shù)據(jù)有變動時回調(diào)消費(fèi)方的NotifyListener服務(wù)的notify方法NotifyListener.notify(List<Url>) 回調(diào)接口傳入所有服務(wù)的提供方的url地址然后將urls轉(zhuǎn)化為invokers, 也就是refer應(yīng)用遠(yuǎn)程服務(wù)到此時引用某個遠(yuǎn)程服務(wù)的RegistryDirectory中有對這個遠(yuǎn)程服務(wù)調(diào)用的所有invokers窖杀。
RegistryDirectory.list(invocation)就是根據(jù)服務(wù)調(diào)用方法獲取所有的遠(yuǎn)程服務(wù)引用的invoker執(zhí)行對象
四、服務(wù)路由
dubbo路由功能貌似用的不多裙士,目的主要是對已注冊的服務(wù)進(jìn)行過濾入客,比如只能調(diào)用某些配置的服務(wù),或者禁用某些服務(wù)腿椎。
1. ConditionRouter條件路由
dubbo-admin 后臺進(jìn)行配置桌硫。
路由代碼入口
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
if (invokers == null || invokers.size() == 0) {
return invokers;
}
try {
if (!matchWhen(url, invocation)) {
return invokers;
}
List<Invoker<T>> result = new ArrayList<Invoker<T>>();
if (thenCondition == null) {
logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
return result;
}
.............................
2. ScriptRouter腳本路由
按照dubbo腳本規(guī)則進(jìn)行編寫,程序識別
五啃炸、軟負(fù)載均衡
1. RandomLoadBalance default
隨機(jī)铆隘,按權(quán)重設(shè)置隨機(jī)概率。權(quán)重default=100
在一個截面上碰撞的概率高南用,但調(diào)用量越大分布越均勻膀钠,而且按概率使用權(quán)重后也比較均勻,有利于動態(tài)調(diào)整提供者權(quán)重裹虫。
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 總個數(shù)
int totalWeight = 0; // 總權(quán)重
boolean sameWeight = true; // 權(quán)重是否都一樣
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight; // 累計(jì)總權(quán)重
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false; // 計(jì)算所有權(quán)重是否一樣
}
}
if (totalWeight > 0 && !sameWeight) {
// 如果權(quán)重不相同且權(quán)重大于0則按總權(quán)重數(shù)隨機(jī)
int offset = random.nextInt(totalWeight);
// 并確定隨機(jī)值落在哪個片斷上
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果權(quán)重相同或權(quán)重為0則均等隨機(jī)
return invokers.get(random.nextInt(length));
}
算法含義
如果所有的服務(wù)權(quán)重都一樣肿嘲,就采用總服務(wù)數(shù)進(jìn)行隨機(jī)。如果權(quán)重不一樣筑公,則按照權(quán)重出隨機(jī)數(shù)雳窟,然后用隨機(jī)數(shù)減去服務(wù)權(quán)重,結(jié)果為負(fù)數(shù)則使用當(dāng)前循環(huán)的服務(wù)匣屡。其實(shí)也就是一個概率性問題 每個服務(wù)的概率就是 當(dāng)前服務(wù)的權(quán)重/ 總服務(wù)權(quán)重
2. RoundRobinLoadBalance
輪循封救,按公約后的權(quán)重設(shè)置輪循比率。
存在慢的提供者累積請求的問題捣作,比如:第二臺機(jī)器很慢誉结,但沒掛,當(dāng)請求調(diào)到第二臺時就卡在那虾宇,久而久之搓彻,所有請求都卡在調(diào)到第二臺上。
該負(fù)載算法維護(hù)著一個方法調(diào)用順序計(jì)數(shù)
private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();
以方法名作為key
輪循分為 普通輪詢和加權(quán)輪詢嘱朽。權(quán)重一樣時旭贬,采用取模運(yùn)算普通輪詢,反之加權(quán)輪詢搪泳。
下面看下具體的實(shí)現(xiàn)
RoundRobinLoadBalance#doSelect
i.普通輪詢
AtomicPositiveInteger sequence = sequences.get(key);
if (sequence == null) {
sequences.putIfAbsent(key, new AtomicPositiveInteger());
sequence = sequences.get(key);
}
//獲取本次調(diào)用的服務(wù)器序號稀轨,并+1
int currentSequence = sequence.getAndIncrement();
//當(dāng)前序號和服務(wù)總數(shù)取模
return invokers.get(currentSequence % length);
ii.加權(quán)輪詢
下面貼下核心實(shí)現(xiàn)代碼。注意幾個變量
weightSum
= 服務(wù)權(quán)重之和
invokerToWeightMap
= 權(quán)重>0的 invoker map
int currentSequence = sequence.getAndIncrement();
if (maxWeight > 0 && minWeight < maxWeight) { // 權(quán)重不一樣
// mod < weightSum岸军,下面for循環(huán)進(jìn)行weight遞減奋刽,weight大的服務(wù)被調(diào)用的概率大
int mod = currentSequence % weightSum;
for (int i = 0; i < maxWeight; i++) {
for (Map.Entry<Invoker<T>, IntegerWrapper> each : invokerToWeightMap.entrySet()) {
final Invoker<T> k = each.getKey();
final IntegerWrapper v = each.getValue();
if (mod == 0 && v.getValue() > 0) {
return k;
}
if (v.getValue() > 0) {
v.decrement();
mod--;
}
}
}
}
可以舉個例子
兩個服務(wù) A 和 B瓦侮,權(quán)重分別是1和2
那么 mod=[0,1,2],經(jīng)過上面的邏輯佣谐,調(diào)用概率是 A B B A B B A B B ..... 顯然B的概率更大一些
3. LeastActiveLoadBalance
最少活躍調(diào)用數(shù)優(yōu)先肚吏,活躍數(shù)指調(diào)用前后計(jì)數(shù)差。使慢的提供者收到更少請求狭魂,因?yàn)樵铰奶峁┱叩恼{(diào)用前后計(jì)數(shù)差會越大罚攀。
每個服務(wù)有一個活躍計(jì)數(shù)器,我們假如有A,B兩個提供者.計(jì)數(shù)均為0.當(dāng)A提供者開始處理請求,該計(jì)數(shù)+1,此時A還沒處理完,當(dāng)處理完后則計(jì)數(shù)-1.而B請求接收到請求處理得很快.B處理完后A還沒處理完,所以此時A,B的計(jì)數(shù)為1,0.那么當(dāng)有新的請求來的時候,就會選擇B提供者(B的活躍計(jì)數(shù)比A小).這就是文檔說的,使慢的提供者收到更少請求。
int leastCount = 0; // 相同最小活躍數(shù)的個數(shù)
int[] leastIndexs = new int[length]; // 相同最小活躍數(shù)的下標(biāo)
i.最小活躍服務(wù)個數(shù)=1雌澄, 該服務(wù)優(yōu)先
if (leastCount == 1) {
// 如果只有一個最小則直接返回
return invokers.get(leastIndexs[0]);
}
ii.最小活躍服務(wù)個數(shù)>1, 最小活躍的服務(wù)按照權(quán)重隨機(jī)
if (!sameWeight && totalWeight > 0) {
// 如果權(quán)重不相同且權(quán)重大于0則按總權(quán)重數(shù)隨機(jī)
int offsetWeight = random.nextInt(totalWeight);
// 并確定隨機(jī)值落在哪個片斷上
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
//權(quán)重越大斋泄,offsetWeight越快減成負(fù)數(shù)
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers.get(leastIndex);
}
}
iii. 最小活躍服務(wù)個數(shù)>1, 權(quán)重相同,服務(wù)個數(shù)隨機(jī)
// 如果權(quán)重相同或權(quán)重為0則均等隨機(jī)
return invokers.get(leastIndexs[random.nextInt(leastCount)]);
4. ConsistentHashLoadBalance
- 一致性 Hash镐牺,相同參數(shù)的請求總是發(fā)到同一提供者炫掐。
- 當(dāng)某一臺提供者掛時,原本發(fā)往該提供者的請求睬涧,基于虛擬節(jié)點(diǎn)募胃,平攤到其它提供者,不會引起劇烈變動宙地。
- 算法參見:http://en.wikipedia.org/wiki/Consistent_hashing
- 缺省只對第一個參數(shù) Hash摔认,如果要修改,請配置
<dubbo:parameter key="hash.arguments" value="0,1" />
- 缺省用 160 份虛擬節(jié)點(diǎn)宅粥,如果要修改参袱,請配置
<dubbo:parameter key="hash.nodes" value="320" />
配置樣例
<dubbo:reference id="demoService" interface="com.youzan.dubbo.api.DemoService" loadbalance="consistenthash">
<!--缺省只對第一個參數(shù) Hash-->
<dubbo:parameter key="hash.arguments" value="0,1" />
<!--缺省用 160 份虛擬節(jié)點(diǎn),-->
<dubbo:parameter key="hash.nodes" value="160" />
</dubbo:reference>
算法解析
ConsistentHashLoadBalance為使用該算法的服務(wù)維護(hù)了一個selectors
,
key=invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName()
eg: com.youzan.dubbo.api.DemoService.sayHello
#com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
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) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
int identityHashCode = System.identityHashCode(invokers);
//獲取該服務(wù)的ConsistentHashSelector秽梅,并跟進(jìn)本次調(diào)用獲取對應(yīng)invoker
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.getIdentityHashCode() != identityHashCode) {
selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation);
}
ConsistentHashSelector作為ConsistentHashLoadBalance的內(nèi)部類抹蚀, 就是具體的一致性hash實(shí)現(xiàn)。
- ConsistentHashSelector內(nèi)部元素
#com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector
//該服務(wù)的所有hash節(jié)點(diǎn)
private final TreeMap<Long, Invoker<T>> virtualInvokers;
//虛擬節(jié)點(diǎn)數(shù)量
private final int replicaNumber;
//該服務(wù)的唯一hashcode企垦,通過System.identityHashCode(invokers)獲取
private final int identityHashCode;
- 如何構(gòu)建該服務(wù)的虛擬節(jié)點(diǎn)环壤?
public ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
// 創(chuàng)建TreeMap 來保存結(jié)點(diǎn)
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
// 生成調(diào)用結(jié)點(diǎn)HashCode
this.identityHashCode = System.identityHashCode(invokers);
// 獲取Url
//dubbo://192.168.0.4:20880/com.youzan.dubbo.api.DemoService?anyhost=true&application=consumer-of-helloworld-app&check=false&class=com.youzan.dubbo.provider.DemoServiceImpl&dubbo=2.5.4&generic=false&hash.arguments=0,1&hash.nodes=160&interface=com.youzan.dubbo.api.DemoService&loadbalance=consistenthash&methods=sayHello&pid=32710&side=consumer×tamp=1527383363936
URL url = invokers.get(0).getUrl();
// 獲取所配置的結(jié)點(diǎn)數(shù),如沒有設(shè)置則使用默認(rèn)值160
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
// 獲取需要進(jìn)行hash的參數(shù)數(shù)組索引钞诡,默認(rèn)對第一個參數(shù)進(jìn)行hash
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]);
}
// 創(chuàng)建虛擬結(jié)點(diǎn)
// 對每個invoker生成replicaNumber個虛擬結(jié)點(diǎn)郑现,并存放于TreeMap中
for (Invoker<T> invoker : invokers) {
for (int i = 0; i < replicaNumber / 4; i++) {
// 根據(jù)md5算法為每4個結(jié)點(diǎn)生成一個消息摘要,摘要長為16字節(jié)128位荧降。
byte[] digest = md5(invoker.getUrl().toFullString() + i);
// 隨后將128位分為4部分接箫,0-31,32-63,64-95,95-128,并生成4個32位數(shù)朵诫,存于long中辛友,long的高32位都為0
// 并作為虛擬結(jié)點(diǎn)的key。
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
代碼如果看的不是很懂剪返,也不用去深究了(我就沒看懂废累,瞻仰了網(wǎng)上大神的文章貼了帖注釋)邓梅,大家可以就粗略的認(rèn)為,這段代碼就是盡可能的構(gòu)建出散列均勻的服務(wù)hash表邑滨。
- 如何從virtualInvokers選取本次調(diào)用的invoker日缨?
// 選擇invoker
public Invoker<T> select(Invocation invocation) {
// 根據(jù)調(diào)用參數(shù)來生成Key
String key = toKey(invocation.getArguments());
// 根據(jù)這個參數(shù)生成消息摘要
byte[] digest = md5(key);
//調(diào)用hash(digest, 0),將消息摘要轉(zhuǎn)換為hashCode掖看,這里僅取0-31位來生成HashCode
//調(diào)用sekectForKey方法選擇結(jié)點(diǎn)殿遂。
Invoker<T> invoker = sekectForKey(hash(digest, 0));
return invoker;
}
private String toKey(Object[] args) {
StringBuilder buf = new StringBuilder();
// 由于hash.arguments沒有進(jìn)行配置,因?yàn)橹蝗》椒ǖ牡?個參數(shù)作為key
for (int i : argumentIndex) {
if (i >= 0 && i < args.length) {
buf.append(args[i]);
}
}
return buf.toString();
}
//根據(jù)hashCode選擇結(jié)點(diǎn)
private Invoker<T> sekectForKey(long hash) {
Invoker<T> invoker;
Long key = hash;
// 若HashCode直接與某個虛擬結(jié)點(diǎn)的key一樣乙各,則直接返回該結(jié)點(diǎn)
if (!virtualInvokers.containsKey(key)) {
// 若不一致,找到一個比傳入的key大的第一個結(jié)點(diǎn)幢竹。
SortedMap<Long, Invoker<T>> tailMap = virtualInvokers.tailMap(key);
// 若不存在耳峦,那么選擇treeMap中第一個結(jié)點(diǎn)
// 使用TreeMap的firstKey方法,來選擇最小上界焕毫。
if (tailMap.isEmpty()) {
key = virtualInvokers.firstKey();
} else {
// 若存在則返回
key = tailMap.firstKey();
}
}
invoker = virtualInvokers.get(key);
return invoker;
}
- 一致性hash環(huán)是什么東東蹲坷?和上面的算法什么關(guān)系?
ConsistentHashSelector.virtualInvokers
這個東西就是我們的服務(wù)hash節(jié)點(diǎn)邑飒,單純的從數(shù)據(jù)結(jié)構(gòu)上的確看不到什么環(huán)狀的存在循签,可以先示意下,當(dāng)前的數(shù)據(jù)結(jié)構(gòu)
我們的服務(wù)節(jié)點(diǎn)只是一個普通的 map數(shù)據(jù)存儲而已疙咸,如何形成環(huán)呢县匠?其實(shí)所謂的環(huán)只是邏輯上的展現(xiàn),ConsistentHashSelector.sekectForKey()
方法里通過 TreeMap.tailMap()撒轮、TreeMap.tailMap().firstKey乞旦、TreeMap.tailMap().firstKey() 結(jié)合case實(shí)現(xiàn)了環(huán)狀邏輯。下面我們畫圖說話题山。
第一步原始數(shù)據(jù)結(jié)構(gòu)兰粉,我們按照hash從小到大排列
A,B,C表示我們提供的服務(wù),改示意圖假設(shè)服務(wù)節(jié)點(diǎn)散列均勻
第二步選擇服務(wù)節(jié)點(diǎn)
i. 假設(shè)本地調(diào)用得到的key=2120, 代碼邏輯(指ConsistentHashSelector.sekectForKey
)走到tailMap.firstKey()
那么讀取到
3986
A服務(wù)
ii.假設(shè)本地調(diào)用得到的key=9991, tailMap為空顶瞳,邏輯走到 virtualInvokers.firstKey()
回到起點(diǎn)
讀取到 1579 A服務(wù)
上述兩部情況基本已經(jīng)能夠描述清楚節(jié)點(diǎn)的選擇邏輯玖姑,至于hash直接命中,那么讀取對應(yīng)的服務(wù)即可慨菱,無需多講焰络。
最后環(huán)狀形成
上面兩部的介紹已經(jīng)描述hash算法,那么我們所謂的環(huán)狀是怎么一回事呢抡柿?其實(shí)也就是為了方便更好的理解這個邏輯舔琅,我們將線性的hash排列作為環(huán)狀,然后hash的選擇按照順時針方向選擇節(jié)點(diǎn)(等價于上面hash比較大兄蘖印)
節(jié)點(diǎn)選擇算法與上面等價备蚓,本圖主要用來示意课蔬,理想的hash環(huán)hash差距應(yīng)該是等差,均勻的排列郊尝。
參考:
https://blog.csdn.net/column/details/learningdubbo.html?&page=1
https://blog.csdn.net/revivedsun/article/details/71022871
http://www.reibang.com/p/53feb7f5f5d9