之前在网上看过一致性hash原理,但是看过就忘记了,根本原因是没有理解透彻也没有实践,最近我负责了公司的会员业务,redis是开发自己搭建的,没有被DBA管理,最近要求迁移,这个工作就落在了我头上,为了确保迁移后缓存的key还能路由到原来的数据上,我把架构和源码研究了一番,发现竟然用了一致性hash,顿时眼前一亮,毕竟之前的系统没有这么用过,简单的就是主从+Sentinel,稍微复杂点用集群将数据分slot存储。
一、一致性hash要解决什么问题
一致性hash算法主要应用于分布式缓存系统中,可以有效地解决分布式存储结构下普通余数Hash算法伸缩性差的问题,可以保证在动态增加和删除节点的情况下尽量有多的请求命中原来路由到的节点。
对于分布式缓存,不同的节点存放不同的数据,先看一下普通余数算法的方式,假设有节点数n,缓存的健key,那么健为key的数据该存储到的节点序号index = hash(key) mod n ,但是当增加和删除节点时n会发生变化,造成命中率下降。
二、一致性hash算法原理
一致性hash算法将全量的缓存空间当作一个环型数据结构,总共有2^32个缓存区,环的起点是0,终点是2^32 - 1,并且起点与终点相连,整数按照顺时针分布,范围是[0, 2^32-1]。如下图所示:
那么如何将key和节点对应起来呢?
1)节点和key必须通过同一种hash算法映射到环上,即通过某种hash算法转换成一个32位的整数
将节点hash到环上,index = hash(node),一般会用节点的ip或者name
将key也hash到环上,index = hash(key)
2)找到顺时针方向离key哈希值最近的节点,即大于等于key哈希值的第一个节点作为存储节点
那么我们来看一下增加、删除节点后对缓存系统的影响。
1.增加节点
如下图,增加node4节点,受到影响的仅仅是node1到node4之间的key,不再归node2管,归node4管理
2.删除节点
比如这张图,我们删除了node3,那么key将会存储到node1,所以受到影响的仅仅是node2到node3之间的key,即前一个节点到该节点之间的key。
如果仅仅是这样增加删除节点的命中率问题解决了,但是如果节点在环上分布不均匀,就会导致各个节点的负载不均衡,比如下图,所有的key都将存储到node2。
为了优化因为节点太少分布不均匀而导致的负载不均衡问题,一致性hash算法引入了虚拟节点的概念。
所谓虚拟节点,就是基于原来的物理节点映射出多个节点再映射到环形空间上,虚拟节点保存的机器信息是相同的,只不过hash到不不同的位置上。
三、jedis包的一致性hash实现源码分析
package redis.clients.util;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import redis.clients.util.Hashing;
import redis.clients.util.SafeEncoder;
import redis.clients.util.ShardInfo;
public class Sharded<R, S extends ShardInfo<R>> {
public static final int DEFAULT_WEIGHT = 1;
private TreeMap<Long, S> nodes;//hash环,保存所有节点,key就是hash后的值,value是节点的信息
private final Hashing algo;//hash算法类
private final Map<ShardInfo<R>, R> resources;
private Pattern tagPattern;
public static final Pattern DEFAULT_KEY_TAG_PATTERN = Pattern.compile("\\{(.+?)\\}");
public Sharded(List<S> shards) {
this(shards, Hashing.MURMUR_HASH);
}
public Sharded(List<S> shards, Hashing algo) {
this.resources = new LinkedHashMap();
this.tagPattern = null;
this.algo = algo;
this.initialize(shards);
}
public Sharded(List<S> shards, Pattern tagPattern) {
this(shards, Hashing.MURMUR_HASH, tagPattern);
}
public Sharded(List<S> shards, Hashing algo, Pattern tagPattern) {
this.resources = new LinkedHashMap();
this.tagPattern = null;
this.algo = algo;
this.tagPattern = tagPattern;
this.initialize(shards);
}
private void initialize(List<S> shards) {//节点的初始化 最终多个物理节点的“虚拟节点”将会在环上交错布局,不一定分布均匀。
this.nodes = new TreeMap();
for(int i = 0; i != shards.size(); ++i) {
ShardInfo shardInfo = (ShardInfo)shards.get(i);
int n;
if(shardInfo.getName() == null) {//判断节点的名称为空 就给默认名称计算hash值
for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {//默认权重为1,每个物理节点有160个虚拟节点
this.nodes.put(Long.valueOf(this.algo.hash("SHARD-" + i + "-NODE-" + n)), shardInfo);
}
} else {//判断节点的名称不为空 就用名称计算hash值
for(n = 0; n < 160 * shardInfo.getWeight(); ++n) {
this.nodes.put(Long.valueOf(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n)), shardInfo);
}
}
this.resources.put(shardInfo, shardInfo.createResource());
}
}
public R getShard(byte[] key) {
return this.resources.get(this.getShardInfo(key));
}
public R getShard(String key) {
return this.resour