數據分片一致性hash

 

一致性hash

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

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

  一致性hash在增長或者刪除節點的時候,受到影響的數據是比較有限的,好比這裏增長一個節點N3,其在環上的位置爲600,所以,原來N2負責的範圍段(400, 800]如今由N2(400, 600] N3(600, 800]負責,所以只須要將記錄R2(id:759), R3(id: 607) 從N2,遷移到N3:java

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

  可是,一致性hash方式在增長節點的時候,只能分攤一個已存在節點的壓力;一樣,在其中一個節點掛掉的時候,該節點的壓力也會被所有轉移到下一個節點。咱們但願的是「一方有難,八方支援」,所以須要在增刪節點的時候,已存在的全部節點都能參與響應,達到新的均衡狀態。node

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

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

  工程中,DynamoCassandra都使用了一致性hash算法,且在比較高的版本中都使用了虛擬節點的概念。在這些系統中,須要考慮綜合考慮數據分佈方式和數據副本,當引入數據副本以後,一致性hash方式也須要作相應的調整, 能夠參加cassandra的相關文檔。apache

具體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]) + "]");
	}
}
相關文章
相關標籤/搜索