前言
又是一個夜黑風高的晚上,帶上無線耳機聽一曲。突然很感慨一句話:生活就像心電圖,一帆風順就證明你掛了。
就如同我們干運維的,覺得很簡單的事情,有時候能干出無限可能
。還是言歸正傳吧,這一次我們來說說stringhash分區算法。
1.hash分區算法
2.stringhash分區算法
3.enum分區算法
4.numberrange分區算法
5.patternrange分區算法
6.date分區算法
7.jumpstringhash算法
StringHash分區算法的配置
<tableRule name="rule_hashString">
<rule>
<columns>name</columns>
<algorithm>func_hashString</algorithm>
</rule>
</tableRule>
<function name="func_hashString" class="StringHash">
<property name="partitionCount">3,2</property>
<property name="partitionLength">3,4</property>
<property name="hashSlice">0:3</property>
</function>
和之前的hash算法一樣。需要在rule.xml中配置tableRule和function。
- tableRule標簽,name對應的是規則的名字,而rule標簽中的columns則對應的分片字段,這個字段必須和表中的字段一致。algorithm則代表了執行分片函數的名字。
- function標簽,name代表分片算法的名字,算法的名字要和上面的tableRule中的
標簽相對應。class:指定分片算法實現類。property指定了對應分片算法的參數。不同的算法參數不同。
1.partitionCount:指定分區的區間數,具體為 C1 +C2 + ... + Cn
2.partitionLength:指定各區間長度,具體區間划分為 [0, L1), [L1, 2L1), ..., [(C1-1)L1, C1L1), [C1L1, C1L1+L2), [C1L1+L2, C1L1+2L2), ... 其中,每一個區間對應一個數據節點。
3.hashSlice:指定參與hash值計算的key的子串。字符串從0開始索引計數
接下來我們來詳細介紹一下StringHash的工作原理。我們以上面的配置為例。
1.在啟動的時候,兩個數組點乘做運算,得到取模數。
2.兩個數組進行叉乘,得出物理分區表。
3.根據hashSlice二維數組,把分片字段的字符串進行截取。
字符串截取的范圍是hashSlice[0]到hashSlice[1]。比如我這里設置0,3。‘buddy'這個字符串就會截取出bud
,類似數據庫中的substring
函數。
4.將截取出來的字符串做hash,這個hash的計算方法我研究了一下dble的源代碼。源代碼如下:
/**
* String hash:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] <br>
* h = 31*h + s.charAt(i); => h = (h << 5) - h + s.charAt(i); <br>
*
* @param start hash for s.substring(start, end)
* @param end hash for s.substring(start, end)
*/
public static long hash(String s, int start, int end) {
if (start < 0) {
start = 0;
}
if (end > s.length()) {
end = s.length();
}
long h = 0;
for (int i = start; i < end; ++i) {
h = (h << 5) - h + s.charAt(i);
}
return h;
}
這段源代碼的意思其實上面有解釋。算法是s[0]31^(n-1) + s[1]31^(n-2) + ... + s[n-1]。然后接下來它說明h = 31*h + s.charAt(i)是等同於h = (h << 5) - h + s.charAt(i)。大家是不是還是雲里霧里的。你可以去看文章結尾關於這一點的詳細解釋。
這里我們把這個公式分解一下,根據上述的公式,我們能推導出下列算術式:
i=0 -> h = 31 * 0 + s.charAt(0)
i=1 -> h = 31 * (31 * 0 + s.charAt(0)) + s.charAt(1)
i=2 -> h = 31 * (31 * (31 * 0 + s.charAt(0)) + s.charAt(1)) + s.charAt(2)
i=3 -> h = 31 * (31 * (31 * (31 * 0 + s.charAt(0)) + s.charAt(1)) + s.charAt(2)) + s.charAt(3)
.......以此內推
假設我們的字符串是"buddy",我們截取0-3字符串,我們來算一下。根據上面的函數來寫段java代碼編譯運行。
public class test {
public static void main(String args[]) {
String Str = new String("buddy");
System.out.println(hash(Str,0,3));
}
public static long hash(String s, int start, int end) {
if (start < 0) {
start = 0;
}
if (end > s.length()) {
end = s.length();
}
long h = 0;
for (int i = start; i < end; ++i) {
h = (h << 5) - h + s.charAt(i);
}
return h;
}
}
[root@mysql5 java]# javac test.java
[root@mysql5 java]# java test
97905
通過運行程序截取字符串buddy,0-3得到的結果是97905。那么這個結果是怎么算出來的。首先截取0,3,最終截取的是三個字符串bud
。索引從0開始計數對應的就是i=2。根據i=2的公式:
i=2 -> h = 31 * (31 * (31 * 0 + s.charAt(0)) + s.charAt(1)) + s.charAt(2)
我們可以查詢ascii表
s.charAt(0),是算"b"這個字母的ASCII值,十進制數字為98
s.charAt(1),是算"u"這個字母的ASCII值,十進制數字為117
s.charAt(1),是算"d"這個字母的ASCII值,十進制數字為100
把上述三個值帶入到公式得出 31 * (31 * (31 * 0 + 98) + 117) + 100 = 97905。正好和我們程序計算的值一樣。
5.對計算出來的值取模,然后落在指定的分區中。
97905 mod 17 =2 根據取模的值,落在了dn1分區,dn1分區是存放(0,3)的。
6.讓我們建表來測試一下,是不是落在第1個分區。
如圖所示,當我們執行插入name='buddy',然后再一次查詢的name='buddy'的時候,直接路由到了第一個分區。和我們之前計算的結果一致。
注意事項
- 該分區算法和hash分區算法有同樣的限制(注意事項3除外)
- 分區字段為字符串類型
后記
今天介紹的stringhash和hash分區算法大致相同,只不過對於字符串需先計算出hash值。該算法有個經典的數字叫31。這個數字大有來頭。《Effective Java》中的一段話說明了為什么要用31,因為31是一個奇質數,如果選擇一個偶數的話,乘法溢出信息將丟失。因為乘2等於移位運算。使用質數的優勢不太明顯,但這是一個傳統。31的一個很好的特性是乘法可以用移位和減法來代替以獲得更好的性能:31*i==(i<<5)-i
。現代的 Java 虛擬機可以自動的完成這個優化。
The value 31 was chosen because it is an odd prime. If it were even and the multiplication overflowed, information would be lost, as multiplication by 2 is equivalent to shifting. The advantage of using a prime is less clear, but it is traditional. A nice property of 31 is that the multiplication can be replaced by a shift and a subtraction for better performance: 31 * i == (i << 5) - i. Modern VMs do this sort of optimization automatically.
如果你前面沒看懂前面那段java代碼,現在應該明白(h << 5) - h的結果其實就等於31*h。
今天到這兒,后續將繼續分享其他的算法。謝謝大家支持!