前言
又是一个夜黑风高的晚上,带上无线耳机听一曲。突然很感慨一句话:生活就像心电图,一帆风顺就证明你挂了。
就如同我们干运维的,觉得很简单的事情,有时候能干出无限可能
。还是言归正传吧,这一次我们来说说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 = 31h + 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。
今天到这儿,后续将继续分享其他的算法。谢谢大家支持!