數據分片一致性hash


 

一致性hash

   一致性hash是將數據按照特征值映射到一個首尾相接的hash環上,同時也將節點(按照IP地址或者機器名hash)映射到這個環上。對於數據,從數據在環上的位置開始,順時針找到的第一個節點即為數據的存儲節點。這里仍然以上述的數據為例,假設id的范圍為[0, 1000],N0, N1, N2在環上的位置分別是100, 400, 800,那么hash環示意圖與數據的分布如下:
      

  可以看到相比於上述的hash方式,一致性hash方式需要維護的元數據額外包含了節點在環上的位置,但這個數據量也是非常小的。

  一致性hash在增加或者刪除節點的時候,受到影響的數據是比較有限的,比如這里增加一個節點N3,其在環上的位置為600,因此,原來N2負責的范圍段(400, 800]現在由N2(400, 600] N3(600, 800]負責,因此只需要將記錄R2(id:759), R3(id: 607) 從N2,遷移到N3:

  不難發現一致性hash方式在增刪的時候只會影響到hash環上響應的節點,不會發生大規模的數據遷移。

  但是,一致性hash方式在增加節點的時候,只能分攤一個已存在節點的壓力;同樣,在其中一個節點掛掉的時候,該節點的壓力也會被全部轉移到下一個節點。我們希望的是“一方有難,八方支援”,因此需要在增刪節點的時候,已存在的所有節點都能參與響應,達到新的均衡狀態。

  因此,在實際工程中,一般會引入虛擬節點(virtual node)的概念。即不是將物理節點映射在hash換上,而是將虛擬節點映射到hash環上。虛擬節點的數目遠大於物理節點,因此一個物理節點需要負責多個虛擬節點的真實存儲。操作數據的時候,先通過hash環找到對應的虛擬節點,再通過虛擬節點與物理節點的映射關系找到對應的物理節點。

  引入虛擬節點后的一致性hash需要維護的元數據也會增加:第一,虛擬節點在hash環上的問題,且虛擬節點的數目又比較多;第二,虛擬節點與物理節點的映射關系。但帶來的好處是明顯的,當一個物理節點失效是,hash環上多個虛擬節點失效,對應的壓力也就會發散到多個其余的虛擬節點,事實上也就是多個其余的物理節點。在增加物理節點的時候同樣如此。

  工程中,DynamoCassandra都使用了一致性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]) + "]");
	}
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM