张伦聪的技术博客 Research And Development

缓存雪崩防范-一致性hash


什么是缓存雪崩

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如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"));
        
    }
}



如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏博主

留言