架构 - 缓存集群方案

版权声明:写文章辛苦,请不要复制粘贴,如果要,请注明来处 https://blog.csdn.net/u012627861/article/details/84337383

缓存集群面临的问题

  • 路由:假设我们的缓存服务器有3台,每台缓存的数据是不相同的,那么我们根据key获取缓存时,该从哪台服务器获取?
  • 分压:在新增服务器时,如何让新增的服务器平均的为各服务器减压
  • 命中率:在新增服务器后,如何保证命中率尽可能的高?

解决方案一:Hash取模路由(不推荐)

上面的问题可以通过取模的方式来进行路由,如下

...
public static String getRoute(String key){
    int cacheIndex = key.hashcode() % 2;
    if(cacheIndex == 0){
        return "A缓存服务器";
    }
    return "B缓存服务器";
}
...

这样可以对固定数量的缓存集群进行路由,如果数量发生改变,例如从2台缓存服务器增加到10台缓存服务器。模2的值和模10的值不一样就会导致路由的结果不正确。路由不正确就会查询数据库,大量的路由不正确就会给数据库增加压力。正因为这种方式在横向扩展的情况下命中率低,所以不推荐此方式进行路由。

解决方案二:Hash环路由(推荐)

Hash环的基本思路是获取所有的服务器节点hash值,然后获取key的hash,与节点的hash进行对比,找出顺时针最近的节点进行存储和读取。例如有以下服务器:

  • A服务器(hash:100)
  • B服务器(hash:120)
  • C服务器(hash:200)

当key的hash为105时,存储在B服务器。当key的hash为121时,存储在C服务器。

命中率的计算(仅供参考)
在《大型网站技术架构》一书中提到hash一致性算法hash环的命中率为n/(n+1),例如从3台服务器扩展到4台服务器,得到命中率为3/4=0.75。而我始终想不通这个公式。如果节点平均分配,这个命中率应该66.66%至100%之间,折中计算这个命中率也应该是83.33%。思路如下:

ABC三台服务器,加入D服务器后介于BC之间,那么只会影响C服务器,AB服务器的数据路由不会有问题,只有在C服务器路由的时候可能会路由至D。AB占用数据比例为66.66%,折中计算C服务器有一半的数据还能命中等同于将一个饼平均分成六份,有五份命中,则得到5/6=0.8333。

思考:为什么不直接全局找出最近的节点,而是要顺时针去找最近的节
点?

如果现在加入D服务器(hash:140),我们希望尽可能的命中率高。假设我们是通过找出最近的节点来路由的。那么key hash在131至170的数据都会落在D服务器,无法命中。如果采用顺时针最近节点的方式路由,那么只有120-140的数据落在D服务器上。而我们在横向扩展时应尽可能高的保证缓存命中率,显然顺时针查找最近节点的方式命中率更高。

思考:加入的D服务器介于BC之间,按照上面提到的思路可以得出D只会给C分担压力,那么如何让D服务器给AB服务器分担压力?
我们可以将一台服务器配置多个节点分布在Hash环上。例如将A服务器分成a1, a2, a3节点,B服务器分成b1, b2, b3节点,C服务器分成c1, c2, c3节点,这些节点都保存了对应的物理服务器信息。假设新加入的3个节点被随机分配在了a1,a2之间,b1,b2之间,c1,c2之间,那么就分别给ABC三台服务器分担了压力。

关于Hash环路由算法实现如下(仅供参考)
注意一:命中率跟节点是否平均分布有关,不平均的节点分布将会导致命中率偏高或偏低。
注意二:该类没有经历过项目的磨砺,内容仅供参考。

package caesar.hash;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;

/**
 * Hash路由,适用于缓存集群等不要求命中率为100%的路由
 * - 通过hash环实现hash路由
 * - 通过节点名称或节点hash值添加节点
 * - 通过二分法实现hash查找
 * @author Caesar Liu
 * @date 2018/11/22 10:37
 */
public class HashRoute {

    // hash环,key为节点hashcode,value为节点服务器信息
    private Map<Integer, Object> hashCircle = new HashMap<Integer, Object>();

    // 节点hash池,用于查找节点hash值
    private List<Integer> nodeHashPool = new ArrayList<Integer>();

    // HashRoute实例
    private static HashRoute instance = null;

    private HashRoute(){ }

    /**
     * 获取HashRoute实例
     * @return
     */
    public static HashRoute getInstance(){
        synchronized (HashRoute.class){
            if(instance == null){
                instance = new HashRoute();
            }
        }
        return instance;
    }

    /**
     * 通过hash值添加节点
     * @param nodeHash
     * @param routeValue
     */
    public void add(int nodeHash, Object routeValue) throws NoSuchAlgorithmException {
        this.hashCircle.put(nodeHash, routeValue);
        this.nodeHashPool.add(nodeHash);
    }

    /**
     * 通过名称添加节点
     * @param nodeName
     * @param routeValue
     */
    public void add(String nodeName, Object routeValue) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance("MD5");
        // 为了均匀的将节点分布在hash环中,将节点名称md5后再获取hashcode
        String nodeNameMd5 = new BigInteger(md.digest(nodeName.getBytes())).toString(16);
        this.hashCircle.put(nodeNameMd5.hashCode(), routeValue);
        this.nodeHashPool.add(nodeNameMd5.hashCode());
    }

    /**
     * 提交节点的变更
     * - 将节点hash进行升序排序
     */
    public void commit(){
        Collections.sort(this.nodeHashPool);
    }

    /**
     * 获取路由值
     * @return
     */
    public Object getRouteValue(String key) throws NoSuchAlgorithmException{
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        // 为了均匀的存储在集群中,将值(如订单id)进行md5后作为key
        String keyMd5 = new BigInteger(md5.digest(key.getBytes())).toString(16);
        return this.hashCircle.get(this.getNodeHashCode(keyMd5));
    }

    /**
     * 获取节点hashcode
     * @param key
     * @return
     */
    private Integer getNodeHashCode(String key){
        int keyHash = key.hashCode();
        // 通过二分法顺时针寻找节点hash值
        int startIndex = 0;
        int endIndex = this.nodeHashPool.size() - 1;
        // 临界点判断(形成Hash闭环并可给二分法提速),如果小于第一个索引值或大于最大索引值,则直接return 0下标节点hash
        if(keyHash <= this.nodeHashPool.get(startIndex)){
            return this.nodeHashPool.get(startIndex);
        }
        if(keyHash > this.nodeHashPool.get(endIndex)){
            return this.nodeHashPool.get(startIndex);
        }
        while(startIndex <= endIndex){
            int index = (startIndex + endIndex) / 2;
            int value = this.nodeHashPool.get(index);
            if(keyHash > value){
                startIndex = index + 1;
            } else if(keyHash < value){
                endIndex = index - 1;
            } else{
                return this.nodeHashPool.get(index);
            }
        }
        // 如果是endIndex左移导致startIndex > endIndex,说明顺时针最近节点hash值为startIndex下的hash值
        // 如果是startIndex右移导致startIndex > endIndex,说明顺时针最近节点hash值为startIndex下的hash值
        return this.nodeHashPool.get(startIndex);
    }

    public static void main(String[] args) throws Exception{
        // 测试数
        final int TEST_COUNT = 100000;
        // 构造路由对象并添加服务器节点,一台服务器构造五个虚拟节点
        HashRoute hr = HashRoute.getInstance();
        hr.add("node1_1", "node1 uri");
        hr.add("node1_2", "node1 uri");
        hr.add("node1_3", "node1 uri");
        hr.add("node1_4", "node1 uri");
        hr.add("node1_5", "node1 uri");
        hr.add("node2_1", "node2 uri");
        hr.add("node2_2", "node2 uri");
        hr.add("node2_3", "node2 uri");
        hr.add("node2_4", "node2 uri");
        hr.add("node2_5", "node2 uri");
        hr.add("node3_1", "node3 uri");
        hr.add("node3_2", "node3 uri");
        hr.add("node3_3", "node3 uri");
        hr.add("node3_4", "node3 uri");
        hr.add("node3_5", "node3 uri");
        // 将节点变更提交
        hr.commit();
        // 记录原有的存储路由
        Map<String, Object> record = new HashMap<String, Object>();
        for(int i = 0; i < TEST_COUNT; i++){
            String key = "" + i;
            record.put(key, hr.getRouteValue(key));
        }
        // 增加一台服务器
        hr.add("node4_1", "node4 uri");
        hr.add("node4_2", "node4 uri");
        hr.add("node4_3", "node4 uri");
        hr.add("node4_4", "node4 uri");
        hr.add("node4_5", "node4 uri");
        hr.commit();
        // 计算命中率
        int matchCount = 0; // 命中数
        for(int i = 0; i < TEST_COUNT; i++){
            String key = "" + i;
            Object routeValue = hr.getRouteValue(key);
            // 如果路由结果相等,则视为命中
            if(routeValue.equals(record.get(key))){
                matchCount++;
            }
        }
        System.out.println("命中率: " + (double)(matchCount)/TEST_COUNT);
    }

}

(完)

猜你喜欢

转载自blog.csdn.net/u012627861/article/details/84337383