什么是缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
什么是一致性hash
一致性hash可以将set进redis集群的key均匀得分布在redis集群中,某一节点宕机不会导致大面积缓存失效。现在我们假设有100台redis data服务器,一份数据101进来的时候,以散列公式hash(i)&100,计算所存放的服务器,假设hash(i) = i,那么数据被散列到标号为1的服务器,然后这个时候服务器新增了一台,然后散列公式为hash(i)%101,这个时候请求访问数据101的时候,被分配至0号服务器,但是其实这个时候数据是在1号服务器的,所以这个时候大量的数据失效了。
先将集群机器使用hash函数散列到一个2^32个点的环中,在采用一致性哈希算法的分布式集群中将新的机器加入,其原理是通过使用与对象存储一样的Hash算法将机器也映射到环中(一般情况下对机器的hash计算是采用机器的IP或者机器唯一的别名作为输入值),然后以顺时针的方向计算,将所有对象存储到离自己最近的机器中。假设现在有NODE1,NODE2,NODE3三台机器,通过Hash算法得到对应的KEY值,映射到环中,其示意图如下:
Hash(NODE1) = KEY1;
Hash(NODE2) = KEY2;
Hash(NODE3) = KEY3;
优点:在增删机器时只需要移动少部分数据即可
虚拟节点
我们引入虚拟节点来解决负载不均衡的问题,可参考以下链接。
一致性Hash(Consistent Hashing)原理剖析
谷歌封装的一致性hash代码
import com.google.common.hash.Hashing;
...
List<String> servers = Arrays.asList("redis服务器A", "redis服务器B", "redis服务器C");
int bucket1 = Hashing.consistentHash(Hashing.md5().hashString("18612345678", Charsets.UTF_8), servers.size());
int bucket2 = Hashing.consistentHash(Hashing.md5().hashString("18600000000", Charsets.UTF_8), servers.size());
int bucket3 = Hashing.consistentHash(Hashing.md5().hashString("13512345678", Charsets.UTF_8), servers.s
System.out.println(servers.get(bucket1));
System.out.println(servers.get(bucket2));
System.out.println(servers.get(bucket3));
...
自定义一致性hash代码
import com.google.common.base.Charsets;
import com.google.common.hash.HashFunction;
import com.google.common.hash.Hashing;
import java.util.ArrayList;
import java.util.Collection;
import java.util.SortedMap;
import java.util.TreeMap;
/**
* 缓存雪崩防止-一致性hash
* Created by zhangluncong on 2018/5/24.
*/
public class ConsistentHash<T> {
/**
* 所用的hash函数
*/
private final HashFunction hashFunction;
/**
* server虚拟节点倍数(100左右比较合理)(虚拟节点=物理节点*numberOfReplicas)
*/
private final int numberOfReplicas;
/**
* server节点分布圆
*/
private final SortedMap<Integer, T> circle = new TreeMap<Integer, T>();
/**
* 初始化一致性hash算法
*
* @param hashFunction
* @param numberOfReplicas
* @param nodes server物理节点集合
*/
private ConsistentHash(HashFunction hashFunction, int numberOfReplicas, Collection<T> nodes) {
this.hashFunction = hashFunction;
this.numberOfReplicas = numberOfReplicas;
for (T node : nodes) {
add(node);
}
}
/**
* 加入server物理节点散列成的所有虚拟节点
*
* @param node
*/
public void add(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.put(hashFunction.hashString(node.toString() + i, Charsets.UTF_8).asInt(), node);
}
}
/**
* 移除server物理节点散列成的所有虚拟节点
*
* @param node
*/
public void remove(T node) {
for (int i = 0; i < numberOfReplicas; i++) {
circle.remove(hashFunction.hashString(node.toString() + i, Charsets.UTF_8).asInt());
}
}
/**
* 获取client对应server物理节点
*
* @param key 虚拟节点
* @return 物理节点
*/
public T get(Object key) {
if (circle.isEmpty()) {
return null;
}
//生成client对应的hash值
int hash = hashFunction.hashString(key.toString(), Charsets.UTF_8).asInt();
//如果没有对应此hash的server节点,获取大于等于此hash后面的server节点;如果还没有,则获取server节点分布圆的第一个节点
if (!circle.containsKey(hash)) {
SortedMap<Integer, T> tailMap = circle.tailMap(hash);
hash = tailMap.isEmpty() ? circle.firstKey() : tailMap.firstKey();
}
System.out.println(hash);
return circle.get(hash);
}
/********************** 测试代码**********start ************************/
public static void main(String[] args) {
HashFunction hf = Hashing.md5();
//物理服务器,可以是ip地址等
ArrayList<String> nodeList = new ArrayList<String>();
nodeList.add("redis服务器A");
nodeList.add("redis服务器B");
nodeList.add("redis服务器C");
ConsistentHash<String> consistentHash = new ConsistentHash<String>(hf, 100, nodeList);
//根据一致性hash算法获取客户端对应的服务器节点
System.out.println(consistentHash.get("18612345678"));
System.out.println(consistentHash.get("18600000000"));
System.out.println(consistentHash.get("13512345678"));
}
}