/**
- 存储节点列表
*/
private List nodes = new ArrayList<>();
@Override
public void addNode(Node node) {
this.nodes.add(node);
}
@Override
public Node lookupNode(String key) {
long k = hash(key);
int index = (int) (k % nodes.size());
return nodes.get(index);
}
@Override
public Long hash(String key) {
CRC32 crc32 = new CRC32();
crc32.update(key.getBytes());
return crc32.getValue();
}
@Override
public void removeNodeUnexpected(Node node) {
nodes.remove(node);
}
}
好,有什么问题呢,测试一下
package cn.enjoy.hash;
import cn.enjoy.hash.HashNodeService;
import cn.enjoy.hash.Node;
import cn.enjoy.hash.NormalHashNodeServiceImpl;
import org.junit.Before;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
public class HashTest {
private HashNodeService nodeService;
@Before
public void init() {
nodeService= new NormalHashNodeServiceImpl();
Node addNode1 = new Node(“node1”, “192.168.0.11”);
Node addNode2 = new Node(“node2”, “192.168.0.12”);
Node addNode3 = new Node(“node3”, “192.168.0.13”);
Node addNode4 = new Node(“node4”, “192.168.0.14”);
Node addNode5 = new Node(“node5”, “192.168.0.15”);
Node addNode6 = new Node(“node6”, “192.168.0.16”);
Node addNode7 = new Node(“node7”, “192.168.0.17”);
Node addNode8 = new Node(“node8”, “192.168.0.18”);
nodeService.addNode(addNode1);
nodeService.addNode(addNode2);
nodeService.addNode(addNode3);
nodeService.addNode(addNode4);
nodeService.addNode(addNode5);
nodeService.addNode(addNode6);
nodeService.addNode(addNode7);
nodeService.addNode(addNode8);
}
@Test
public void test2() {
//用于检查数据分布情况
Map<String, Integer> countmap = new HashMap<>();
Node node = null;
for (int i = 1; i <= 100000; i++) {
String key = String.valueOf(i);
node = nodeService.lookupNode(key);
node.cacheString(key, “TEST_VALUE”);
String k = node.getIp();
Integer count = countmap.get(k);
if (count == null) {
count = 1;
countmap.put(k, count);
} else {
count++;
countmap.put(k, count);
}
}
System.out.println(“初始化数据分布情况:” + countmap);
// 正常情况下的去获取数据,命中率
int hitcount = 0;
for (int i = 1; i <= 100000; i++) {
String key = String.valueOf(i);
node = nodeService.lookupNode(key);
if (node != null) {
String value = node.getCacheValue(key);
if (value != null) {
hitcount++;
}
}
}
double h = Double.parseDouble(String.valueOf(hitcount))/ Double.parseDouble(String.valueOf(100000));
System.out.println(“初始化缓存命中率:”+ h);
Node addNode9 = new Node(“node9”, “192.168.0.19”);
nodeService.addNode(addNode9);
hitcount = 0;
for (int i = 1; i <= 100000; i++) {
String key = String.valueOf(i);
node = nodeService.lookupNode(key);
if (node != null) {
String value = node.getCacheValue(key);
if (value != null) {
hitcount++;
}
}
}
h = Double.parseDouble(String.valueOf(hitcount))/ Double.parseDouble(String.valueOf(100000));
System.out.println(“增加一个节点后缓存命中率:”+ h);
}
}
本来8个服务器(node),使用hash算法从集群里取值命中率100%,当新增了一个节点后可怜兮兮,命中率直接变成了10%左右,这个时候普通hash的问题显露无疑了,怎么办,试试一致性hash.
1.1.2. 一致性hash算法
一致性hash是为了解决普通hash的问题,使用一个环状结构,具体可以分层4步骤。
1.1.2.1. 步骤
1.1.2.1.1. 构建环形hash空间
说明:
一致性hash算法通过一个叫作一致性hash环的数据结构实现
这个环的起点是0,终点是2^32 - 1,并且起点与终点连接,环的中间的整数按逆时针分布
这个环的整数分布范围是[0, 2^32-1]
1.1.2.1.2. 把对象映射到hash空间
假设现在我们有4个对象,分别为o1,o2,o3,o4,
使用hash函数计算这4个对象的hash值(范围为0 ~ 2^32-1)
1.1.2.1.3. 把cache节点映射到hash空间
使用同样的hash函数,我们将机器也放置到hash环上。
假设我们有三台缓存机器,分别为 c1,c2,c3
使用hash函数计算这3台机器的hash值
1.1.2.1.4. 对象映射到cache节点
将对象和机器都放置到同一个hash环后
在hash环上顺时针查找距离这个对象的hash值最近的机器,即是这个对象所属的机器。
1.1.2.1.5. 增加一个节点的情况
增加机器c4的部署并将机器c4加入到hash环的机器c3与c2之间。
只有对象o4被重新分配到了c4
其他对象仍在原有机器上
1.1.2.1.6. 删除一个节点的情况
将机器c1下线(当然,也有可能是机器c1宕机)
只有对象o2被重新分配到机器c3
其他对象仍在原有机器上
1.1.2.1.7. 虚拟节点
仅仅使用真实的节点来形成环的结构可能会导致数据的倾斜,为了解决这种数据不对称的现象,可以在原理缓存的机器的基础上,增加相应的虚拟节点,这样数据就会均匀的打散到其他节点中。
这个时候客户端进行数据存取的时候找的就是虚拟节点,而不是真实的物理节点服务器。
这样增加节点会删除节点虽然命中率会降低,却很好的解决了数据倾斜问题。
1下线(当然,也有可能是机器c1宕机)
只有对象o2被重新分配到机器c3
其他对象仍在原有机器上
1.1.2.1.7. 虚拟节点
仅仅使用真实的节点来形成环的结构可能会导致数据的倾斜,为了解决这种数据不对称的现象,可以在原理缓存的机器的基础上,增加相应的虚拟节点,这样数据就会均匀的打散到其他节点中。
[外链图片转存中…(img-b374zjpf-1716912834796)]
这个时候客户端进行数据存取的时候找的就是虚拟节点,而不是真实的物理节点服务器。
[外链图片转存中…(img-avZzQsYp-1716912834797)]
这样增加节点会删除节点虽然命中率会降低,却很好的解决了数据倾斜问题。