总之,相比于普通的哈希算法,一致性哈希算法对于节点的动态增删,具有一定的容错性和可扩展性。
/**
* MurMurHash算法,是非加密HASH算法,性能很高,
* 比传统的CRC32,MD5,SHA-1(这两个算法都是加密HASH算法,复杂度本身就很高,带来的性能上的损害也不可避免)
* 等HASH算法要快很多,而且据说这个算法的碰撞率很低.
* http://murmurhash.googlepages.com/
*/
private Long hash(String key) {
ByteBuffer buf = ByteBuffer.wrap(key.getBytes());
int seed = 0x1234ABCD;
ByteOrder byteOrder = buf.order();
buf.order(ByteOrder.LITTLE_ENDIAN);
long m = 0xc6a4a7935bd1e995L;
int r = 47;
long h = seed ^ (buf.remaining() * m);
long k;
while (buf.remaining() >= 8) {
k = buf.getLong();
k *= m;
k ^= k >>> r;
k *= m;
h ^= k;
h *= m;
}
if (buf.remaining() > 0) {
ByteBuffer finish = ByteBuffer.allocate(8).order(
ByteOrder.LITTLE_ENDIAN);
// for big-endian version, do this first:
// finish.position(8-buf.remaining());
finish.put(buf).rewind();
h ^= finish.getLong();
h *= m;
}
h ^= h >>> r;
h *= m;
h ^= h >>> r;
buf.order(byteOrder);
return h;
}
package redis.cn;
import java.nio.charset.Charset;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
public class ConsistentHash {
// ------------------ 一致性哈希算法的java实现 ------------------
private SortedMap ketamaNodes = new TreeMap();
private int numberOfReplicas = 1024;
// 这里使用了谷歌的jar包 -- guava-18.0.jar
private HashFunction hashFunction = Hashing.md5();
private List nodes;
private volatile boolean init = false; //标志是否初始化完成
// 有参数构造函数
public ConsistentHash(int numberOfReplicas,List nodes){
this.numberOfReplicas = numberOfReplicas;
this.nodes = nodes;
init();
}
// 根据key的哈希值,找到最近的一个节点(服务器)
public String getNodeByKey(String key){
if(!init)
throw new RuntimeException("init uncomplete...");
// 注意,这里是NIO包 java.nio.charset.Charset
byte[] digest = hashFunction.hashString(key, Charset.forName("UTF-8")).asBytes();
long hash = hash(digest,0);
//如果找到这个节点,直接取节点,返回
if(!ketamaNodes.containsKey(hash)){
//得到大于当前key的那个子Map,然后从中取出第一个key,就是大于且离它最近的那个key
SortedMap tailMap = ketamaNodes.tailMap(hash);
if(tailMap.isEmpty()){
hash = ketamaNodes.firstKey();
}else{
hash = tailMap.firstKey();
}
}
return ketamaNodes.get(hash);
}
// 新增节点
public synchronized void addNode(String node){
init = false;
nodes.add(node);
init();
}
private void init(){
//对所有节点,生成numberOfReplicas个虚拟节点
for(String node:nodes){
//每四个虚拟节点为1组
for(int i=0;i
//少量优化性能
public ThreadLocal md5Holder = new ThreadLocal();
public static final Hashing MD5 = new Hashing() {
public long hash(String key) {
return hash(SafeEncoder.encode(key));
}
// sharding使用的哈希算法是MD5
public long hash(byte[] key) {
try {
if (md5Holder.get() == null) {
md5Holder.set(MessageDigest.getInstance("MD5"));
}
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("++++ no md5 algorythm found");
}
MessageDigest md5 = md5Holder.get();
md5.reset();
md5.update(key);
//获得MD5字节序列
byte[] bKey = md5.digest();
//前四个字节作为计算参数,最终获得一个32位int值.
//此种计算方式,能够确保key的hash值更加“随机”/“离散”
//如果hash值过于密集,不利于一致性hash的实现(特别是有“虚拟节点”设计时)
long res = ((long) (bKey[3] & 0xFF) << 24)
| ((long) (bKey[2] & 0xFF) << 16)
| ((long) (bKey[1] & 0xFF) << 8)
| (long) (bKey[0] & 0xFF);
return res;
}
};
//shards列表为客户端提供了所有redis-server配置信息,包括:ip,port,weight,name
//其中weight为权重,将直接决定“虚拟节点”的“比例”(密度),权重越高,在存储是被hash命中的概率越高
//--其上存储的数据越多。
//其中name为“节点名称”,jedis使用name作为“节点hash值”的一个计算参数。
//---
//一致性hash算法,要求每个“虚拟节点”必须具备“hash值”,每个实际的server可以有多个“虚拟节点”(API级别)
//其中虚拟节点的个数= “逻辑区间长度” * weight,每个server的“虚拟节点”将会以“hash”的方式分布在全局区域中
//全局区域总长为2^32.每个“虚拟节点”以hash值的方式映射在全局区域中。
// 环形:0-->vnode1(:1230)-->vnode2(:2800)-->vnode3(400000)---2^32-->0
//所有的“虚拟节点”将按照其”节点hash“顺序排列(正序/反序均可),因此相邻两个“虚拟节点”之间必有hash值差,
//那么此差值,即为前一个(或者后一个,根据实现而定)“虚拟节点”所负载的数据hash值区间。
//比如hash值为“2000”的数据将会被vnode1所接受。
private void initialize(List shards){
//虚拟节点,采取TreeMap存储:排序,二叉树
nodes = new TreeMap();
for (int i = 0; i != shards.size(); ++i) {
final S shardInfo = shards.get(i);
if (shardInfo.getName() == null)
//当没有设置“name”是,将“SHARD-NODE”作为“虚拟节点”hash值计算的参数
//"逻辑区间步长"为160,为什么呢??
//最终多个server的“虚拟节点”将会交错布局,不一定非常均匀。
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo);
}
else
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo);
}
resources.put(shardInfo, shardInfo.createResource());
}
}
public R getShard(String key) {
return resources.get(getShardInfo(key));
}
public S getShardInfo(byte[] key) {
//获取>=key的“虚拟节点”的列表
SortedMap tail = nodes.tailMap(algo.hash(key));
//如果不存在“虚拟节点”,则将返回首节点。
if (tail.size() == 0) {
return nodes.get(nodes.firstKey());
}
//如果存在,则返回符合(>=key)条件的“虚拟节点”的第一个节点
return tail.get(tail.firstKey());
}
package redis.cn;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.JedisShardInfo;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
/**
* @author yangcq
* @category jedis也是一致性哈希算法的一种实现。搭建redis分布式集群,可以使用jedis。
*/
public class ShardedRedis {
// 除了jdk自带的工具包以后,还需要导入下面2个jar包
// commons-pool2-2.0.jar
// jedis-2.4.2.jar
public static void main(String[] args){
// jedis配置参数
GenericObjectPoolConfig genericObjectPoolConfig = new GenericObjectPoolConfig();
genericObjectPoolConfig.setMaxTotal(1000);
genericObjectPoolConfig.setMaxIdle(500);
List jedisShardInfoList = new ArrayList();
JedisShardInfo jedisShardInfo1 = new JedisShardInfo("127.0.0.1",1234);
JedisShardInfo jedisShardInfo2 = new JedisShardInfo("127.0.0.1",1235);
JedisShardInfo jedisShardInfo3 = new JedisShardInfo("127.0.0.1",1236);
jedisShardInfoList.add(jedisShardInfo1);
jedisShardInfoList.add(jedisShardInfo2);
jedisShardInfoList.add(jedisShardInfo3);
ShardedJedisPool shardedJedisPool = new ShardedJedisPool(genericObjectPoolConfig,jedisShardInfoList);
set("key1","value1",shardedJedisPool);
set("key2","value2",shardedJedisPool);
set("key3","value3",shardedJedisPool);
set("key4","value4",shardedJedisPool);
set("key5","value5",shardedJedisPool);
// jedis隐藏了实现一致性哈希算法的细节,只是给我们提供了简单的接口调用,就可以实现redis分布式集群的搭建
// 那么jedis到底是如何实现一致性哈希算法的呢?
}
public static void set(String key,String value,ShardedJedisPool pool){
// 从共享资源池中获取redis实例
ShardedJedis shardedJedis = pool.getResource();
// 赋值
shardedJedis.set(key,value);
pool.returnResource(shardedJedis);
}
}
public Sharded(List shards, Hashing algo, Pattern tagPattern) {
this.algo = algo;
this.tagPattern = tagPattern;
initialize(shards);
}
//初始化哈希环
private void initialize(List shards) {
nodes = new TreeMap();
for (int i = 0; i != shards.size(); ++i) {
final S shardInfo = shards.get(i);
if (shardInfo.getName() == null)
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n),
shardInfo);
}
else
for (int n = 0; n < 160 * shardInfo.getWeight(); n++) {
nodes.put(
this.algo.hash(shardInfo.getName() + "*"
+ shardInfo.getWeight() + n), shardInfo);
}
resources.put(shardInfo, shardInfo.createResource());
}
}
//将key,value存储到相应的shard
public String set(String key, String value) {
Jedis j = getShard(key);
return j.set(key, value);
}
public R getShard(String key) {
return resources.get(getShardInfo(key));
}
//根据key获取shard
public S getShardInfo(byte[] key) {
SortedMap tail = nodes.tailMap(algo.hash(key));
if (tail.isEmpty()) {
return nodes.get(nodes.firstKey());
}
return tail.get(tail.firstKey());
}