一致性hash
可以看到相比於上述的hash方式,一致性hash方式需要維護的元數據額外包含了節點在環上的位置,但這個數據量也是非常小的。
一致性hash在增加或者刪除節點的時候,受到影響的數據是比較有限的,比如這里增加一個節點N3,其在環上的位置為600,因此,原來N2負責的范圍段(400, 800]現在由N2(400, 600] N3(600, 800]負責,因此只需要將記錄R2(id:759), R3(id: 607) 從N2,遷移到N3:
但是,一致性hash方式在增加節點的時候,只能分攤一個已存在節點的壓力;同樣,在其中一個節點掛掉的時候,該節點的壓力也會被全部轉移到下一個節點。我們希望的是“一方有難,八方支援”,因此需要在增刪節點的時候,已存在的所有節點都能參與響應,達到新的均衡狀態。
因此,在實際工程中,一般會引入虛擬節點(virtual node)的概念。即不是將物理節點映射在hash換上,而是將虛擬節點映射到hash環上。虛擬節點的數目遠大於物理節點,因此一個物理節點需要負責多個虛擬節點的真實存儲。操作數據的時候,先通過hash環找到對應的虛擬節點,再通過虛擬節點與物理節點的映射關系找到對應的物理節點。
引入虛擬節點后的一致性hash需要維護的元數據也會增加:第一,虛擬節點在hash環上的問題,且虛擬節點的數目又比較多;第二,虛擬節點與物理節點的映射關系。但帶來的好處是明顯的,當一個物理節點失效是,hash環上多個虛擬節點失效,對應的壓力也就會發散到多個其余的虛擬節點,事實上也就是多個其余的物理節點。在增加物理節點的時候同樣如此。
工程中,Dynamo、Cassandra都使用了一致性hash算法,且在比較高的版本中都使用了虛擬節點的概念。在這些系統中,需要考慮綜合考慮數據分布方式和數據副本,當引入數據副本之后,一致性hash方式也需要做相應的調整, 可以參加cassandra的相關文檔。
具體Java實現:將真實節點虛擬節點以hashcode為key放入map中並根據hashcode值排序,根據參數的hashcode獲取大於該hashcode的子map集合,這個子map集合的第一個節點就是要命中的節點,如果沒有取到子map就獲取大map的第一個節點
https://www.cnblogs.com/xybaby/p/7076731.html
http://www.jb51.net/article/124819.htm
String s =“Java”,那么計算機會先計算散列碼,然后放入相應的數組中,數組的索引就是從散列碼計算來的,然后再裝入數組里的容器里,如List.這就相當於把你要存的數據分成了幾個大的部分,然后每個部分存了很多值, 你查詢的時候先查大的部分,再在大的部分里面查小的,這樣就比先行查詢要快很多
MongoDB
哈希算法:
可以將任意長度的二進制值映射為較短的,固定長度的二進制值。我們把這個二進制值成為哈希值
哈希值的特點:
* 哈希值是二進制值;
* 哈希值具有一定的唯一性;
* 哈希值極其緊湊;
* 要找到生成同一個哈希值的2個不同輸入,在一定時間范圍內,是不可能的。
哈希表:
哈希表是一種數據機構。哈希表根據關鍵字(key),生成關鍵字的哈希值,然后通過哈希值映射關鍵字對應的值。哈希表存儲了多
余的key(我們本可以只存儲值的),是一種用空間換時間的做法。在內存足夠的情況下,這種“空間換時間”的做法是值得的。哈希表的
產生,靈感來源於數組。我們知道,數組號稱查詢效率最高的數據結構,因為不管數組的容量多大,查詢的時間復雜度都是O(1)。如果
所有的key都是不重復的整數,那么這就完美了,不需要新增一張哈希表,來做關鍵字(key)到值(value)的映射。但是,如果key是
字符串,情況就不一樣了。我們必須要來建一張哈希表,進行映射。
數據庫索引的原理,其實和哈希表是相同的。數據庫索引也是用空間換時間的做法
//String的hash值計算 哈希算法在String類中的應用
@Test
public void test1(){
String str = "qaz";
char value[] = str.toCharArray();
int h = 0;
if ( value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
}
System.out.println(h);
}
char類型是可以運算的因為char在ASCII等字符編碼表中有對應的數值
System.out.println('a'+" "+(0+'q')+" "+(0+'a')+" :"+('a'+'q'));
a 113 97 :210
就拿jdk中String類的哈希方法來舉例,字符串"gdejicbegh"與字符串"hgebcijedg"具有相同的hashCode()返回值-801038016,並且它們具有reverse的關系。這個例子說明了用jdk中默認的hashCode方法判斷字符串相等或者字符串回文,都存在反例。
因為不同的對象可能會生成相同的hashcode值
兩個對象的hashcode值不等,則必定是兩個不同的對象
hash權重算法的要素及原理:
大家都知道,計算機的乘法涉及到移位計算。當一個數乘以2時,就直接拿該數左移一位即可!選擇31原因是因為31是一個素數!
所謂素數:
質數又稱素數。指在一個大於1的自然數中,除了1和此整數自身外,沒法被其他自然數整除的數。
在存儲數據計算hash地址的時候,我們希望盡量減少有同樣的hash地址,所謂“沖突”。如果使用相同hash地址的數據過多,那么這些數據所組成的hash鏈就更長,從而降低了查詢效率!所以在選擇系數的時候要選擇盡量長(31 = 11111[2])的系數並且讓乘法盡量不要溢出(如果選擇大於11111的數,很容易溢出)的系數,因為如果計算出來的hash地址越大,所謂的“沖突”就越少,查找起來效率也會提高。
31的乘法可以由i*31== (i<<5)-1來表示,現在很多虛擬機里面都有做相關優化,使用31的原因可能是為了更好的分配hash地址,並且31只占用5bits!
在java乘法中如果數字相乘過大會導致溢出的問題,從而導致數據的丟失.
而31則是素數(質數)而且不是很長的數字,最終它被選擇為相乘的系數的原因不過與此!
.hashCode方法的作用
對於包含容器類型的程序設計語言來說,基本上都會涉及到hashCode。在Java中也一樣,hashCode方法的主要作用是為了配合基於散列的集合一起正常運行,這樣的散列集合包括HashSet、HashMap以及HashTable。
為什么這么說呢?考慮一種情況,當向集合中插入對象時,如何判別在集合中是否已經存在該對象了?(注意:集合中不允許重復的元素存在)
也許大多數人都會想到調用equals方法來逐個進行比較,這個方法確實可行。但是如果集合中已經存在一萬條數據或者更多的數據,如果采用equals方法去逐一比較,效率必然是一個問題。此時hashCode方法的作用就體現出來了,當集合要添加新的對象時,先調用這個對象的hashCode方法,得到對應的hashcode值,實際上在HashMap的具體實現中會用一個table保存已經存進去的對象的hashcode值,如果table中沒有該hashcode值,它就可以直接存進去,不用再進行任何比較了;如果存在該hashcode值, 就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就散列其它的地址,所以這里存在一個沖突解決的問題,這樣一來實際調用equals方法的次數就大大降低了,說通俗一點:Java中的hashCode方法就是根據一定的規則將與對象相關的信息(比如對象的存儲地址,對象的字段等)映射成一個數值,這個數值稱作為散列值。下面這段代碼是java.util.HashMap的中put方法的具體實現
put方法是用來向HashMap中添加新的元素,從put方法的具體實現可知,會先調用hashCode方法得到該元素的hashCode值,然后查看table中是否存在該hashCode值,如果存在則調用equals方法重新確定是否存在該元素,如果存在,則更新value值,否則將新的元素添加到HashMap中。從這里可以看出,hashCode方法的存在是為了減少equals方法的調用次數,從而提高程序效率
設計一個類的時候為需要重寫equals方法,比如String類,但是千萬要注意,在重寫equals方法的同時,必須重寫hashCode方法
比如設計一個peple類equals方法為 return this.name.equals(((People)obj).name) && this.age== ((People)obj).age; 當把一個people實例作為key放入hashmap再去取的時候(new一個相同姓名年齡的對象)取不到,因為兩個實例的hashcode不一致,具體參考hashmap的get方法,如果重寫hashcode的方法則沒問題return name.hashCode()*37+age;但是,如果name值經常變換,equals方法和hashCode方法中不要依賴於該字段
public static void main(String[] args) {
People p1 = new People("Jack", 12);
System.out.println(p1.hashCode());
HashMap<People, Integer> hashMap = new HashMap<People, Integer>();
hashMap.put(p1, 1);
p1.setAge(13);
System.out.println(hashMap.get(p1));
}
這段代碼輸出的結果為“null”,想必其中的原因大家應該都清楚了。
因此,在設計hashCode方法和equals方法的時候,如果對象中的數據易變,則最好在equals方法和hashCode方法中不要依賴於該字段
package cn.com.gome.gcoin.util;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* @author cyq
* 一致性性hash獲取對應表
*/
public class ConsistentHashingWithTable {
//自定義分表數量,原引用gcoin-commons包的常量,但是影響spa轉移系統,注意后續維護時候保持統一
private static int TRANSACTION_TABLE_NUM = 20;
// 待添加入Hash環的交易表列表
private static String[] transactionTable = new String[TRANSACTION_TABLE_NUM];
static{
for(int ci=0;ci<TRANSACTION_TABLE_NUM;ci++){
transactionTable[ci] = "tbl_account_transaction"+ci;
}
}
// key表示交易表的hash值,value表示交易表
private static SortedMap<Integer, String> sortedMap = new TreeMap<Integer, String>();
//虛擬節點的數目,這里寫死,為了演示需要,一個真實結點對應10個虛擬節點
private static final int VIRTUAL_NODES = 10;
// 程序初始化,將所有的交易表放入sort交易表ap中
static {
for (int i = 0; i < transactionTable.length; i++) {
int hash = getHash(transactionTable[i]);
System.out.println("[" + transactionTable[i] + "]加入集合中, 其Hash值為"
+ hash);
sortedMap.put(hash, transactionTable[i]);
//再添加虛擬節點,遍歷LinkedList使用foreach循環效率會比較高
for(int j=0; j<VIRTUAL_NODES; j++){
String virtualNodeName = transactionTable[i] + "&&VN" + String.valueOf(j);
int hashVN = getHash(virtualNodeName);
System.out.println("虛擬節點[" + virtualNodeName + "]被添加, hash值為" + hashVN);
sortedMap.put(hashVN, transactionTable[i]);
}
}
}
// 得到應當路由到的結點
public static String getServer(String key) {
// 得到該key的hash值
int hash = getHash(key);
// 得到大於該Hash值的所有Map
SortedMap<Integer, String> subMap = sortedMap.tailMap(hash);
if (subMap.isEmpty()) {
// 如果沒有比該key的hash值大的,則從第一個node開始
Integer i = sortedMap.firstKey();
// 返回對應的交易表
return sortedMap.get(i);
} else {
// 第一個Key就是順時針過去離node最近的那個結點
Integer i = subMap.firstKey();
// 返回對應的交易表
return subMap.get(i);
}
}
// 使用FNV1_32_HASH算法計算交易表的Hash值,這里不使用重寫hashCode的方法,最終效果沒區別
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;
}
public static void main(String[] args) {
String[] keys = { "73968928317", "73099946651", "72563328728",
"73967405000", "73968349990", "72112754519", "72088646347",
"74728589363", "73955634071", "73099946613", "72563228728",
"73967477000", "73968649990", "72112769519", "72088796347",
"74728333363", "73955688071" };
for (int i = 0; i < keys.length; i++)
System.out.println("[" + keys[i] + "]的hash值為" + getHash(keys[i])+ ", 被路由到結點[" + getServer(keys[i]) + "]");
}
}
