四、memcache的分布式 一致性hash
一致性hash算法
这个,看到好多地方都讲了。。
当年面试阿里就被问到了。。。 还把这个和 hashMap 里的hash 搞混了。。唉。。。
简单说,就是为了实现把应用的 所有Key, 分散到多个 memcache的算法。
最基本的 分散方式就是取余。。。 但取余对于 memcache 的增减,影响太大了。。。数量一变,全都完蛋。
所以一致性hash, 就采用圆环分段式。。。 将圆环的一段都归为落到自己的节点。
然后,引申出来 虚拟节点。。。。这样节点少的时候,可以分配的更均匀当增加memcache的 时候, 要迁移的key 更加少一些。
(这个一致性hash 和 hashMap 里的hash,扩容,equals概念,一点关系都没有)
课后扩展:
找一个memcache库,读一读 一致性hash的具体代码实现。
spymemcached 默认是用取余的 hash选择。代码在 DefaultConnectionFactory
点击查看代码
public NodeLocator createLocator(List<MemcachedNode> nodes) {
return new ArrayModNodeLocator(nodes, getHashAlg());
}
选择连接的时候,简单粗暴:
点击查看代码
private int getServerForKey(String key) {
int rv = (int) (hashAlg.hash(key) % nodes.length);
assert rv >= 0 : "Returned negative key for key " + key;
assert rv < nodes.length : "Invalid server number " + rv + " for key "
+ key;
return rv;
}
然后是一致性hash算法的实现 KetamaLocator里面 setKetamaNodes方法。
默认 numReps = 160, 就是为每个 memcacheNode,在圆环上生成160 个虚拟节点。
原理就是 拼装出 160个 虚拟节点的key, 然后hash得到一个数字。 (md5 因为出16字节。可以分为4个数字, 所以一次出4个)
最后 newNodeMap 就是一个 160个kv 的map, key是一个数字,value都是同一个memcacheNode。
点击查看代码
// Ketama does some special work with md5 where it reuses chunks.
// Check to be backwards compatible, the hash algorithm does not
// matter for Ketama, just the placement should always be done using
// MD5
if (hashAlg == DefaultHashAlgorithm.KETAMA_HASH) {
for (int i = 0; i < numReps / 4; i++) {
for(long position : ketamaNodePositionsAtIteration(node, i)) {
newNodeMap.put(position, node);
getLogger().debug("Adding node %s in position %d", node, position);
}
}
} else {
for (int i = 0; i < numReps; i++) {
newNodeMap.put(hashAlg.hash(config.getKeyForNode(node, i)), node);
}
}
查找key的时候,看 KetamaNodelocator的 getPrimary, 先计算key的hash值。然后在node环的treeMap中取tailMap
(tailMap(K fromKey)方法用于返回此映射中键大于或等于 fromKey 的部分的视图。)
tailMap 有值就取下一个 node。
如果找不到,说明落到最后一个节点后面,就取整个环的第一个node
点击查看代码
public MemcachedNode getPrimary(final String k) {
MemcachedNode rv = getNodeForKey(hashAlg.hash(k));
assert rv != null : "Found no node for key " + k;
return rv;
}
long getMaxKey() {
return getKetamaNodes().lastKey();
}
MemcachedNode getNodeForKey(long hash) {
final MemcachedNode rv;
if (!ketamaNodes.containsKey(hash)) {
// Java 1.6 adds a ceilingKey method, but I'm still stuck in 1.5
// in a lot of places, so I'm doing this myself.
SortedMap<Long, MemcachedNode> tailMap = getKetamaNodes().tailMap(hash);
if (tailMap.isEmpty()) {
hash = getKetamaNodes().firstKey();
} else {
hash = tailMap.firstKey();
}
}
rv = getKetamaNodes().get(hash);
return rv;
}
项目里实际使用的时候,上层再封装了一下。分布式hash 获取到的是个集群对象memcacheCluster,而不是具体的 memcacheClient。
memcacheCluster 里面包含一组 readClients 和 writeClients。
再根据是读还是写,选其中的随机一个可连接的读。
或者对所有可写的进行更新。
这样封装的好处,是方便配置里进行缓存的迁移替换。
将新接入的mecache 加入 cluster,设置为可写。
写一段时间后,将其设置为可读。。。然后将准备要下线的memche,设置为不可读写。再下掉即可。
参考:
https://my.oschina.net/astute/blog/93492
https://www.jianshu.com/p/f78a31725582
参考资料:
https://cloud.tencent.com/developer/article/2040911?from=article.detail.1477161