【180409】一次LocalCache的选择及其原理

3,390 阅读5分钟
原文链接: www.jianshu.com

概述

互联网架构设计的五大要素:高性能、高可用、可伸缩性、可扩展性、安全。

如何做到高性能、高可用,缓存是一大助力。我们知道,绝大部分的时候,读数据写数据符合二八定律。并且读数据中,百分之二十是数据被经常读取(热数据)。那么我们解决这百分之二十的数据的方法就可以取得很好的一个性能。

以一句比较戏谑的话说说本质的东西:

互联网架构设计中没有什么不能通过一层抽象层(代理)解决的,如果有,那就两层。

他山之石

缓存分类

从很多互联网架构设计中可以看到,从用户在浏览器上输入网址开始,经历了太多的缓存。我大概列举一下:

  1. 输入网址后,查询浏览器缓存
  2. 查询浏览器dns缓存
  3. 查询操作系统dns缓存
  4. 请求dns服务器,查询dns服务器缓存
  5. 获得ip,静态资源走cdn缓存。动态数据走服务器
  6. 如果配置了页面缓存,走页面缓存
  7. 如果配置了本地缓存(localcache),走本地缓存
  8. 如果配置了分布式缓存(如redis等等),走分布式缓存
  9. 数据库操作,数据库缓存

这里,我想主要讲讲2种localcache的选择,为什么要引入localcache,可以用《互联网架构的三板斧》中的一句话来说明:

数据访问热点,比如Detail中对某些热点商品的访问度非常高,即使是Tair缓存这种Cache本身也有瓶颈问题,一旦请求量达到单机极限也会存在热点保护问题。有时看起来好像很容易解决,比如说做好限流就行,但你想想一旦某个热点触发了一台机器的限流阀值,那么这台机器Cache的数据都将无效,进而间接导致Cache被击穿,请求落地应用层数据库出现雪崩现象。这类问题需要与具体Cache产品结合才能有比较好的解决方案,这里提供一个通用的解决思路,就是在Cache的client端做本地Localcache,当发现热点数据时直接Cache在client里,而不要请求到Cache的Server。

还有一个原因是,在做秒杀的时候,我可以在每一台应用服务器中设置一个有失效时间的商品剩余数量的计数器,以达到尽可能在调用链前面拦截非有效请求。

分布式缓存

如何部署分布式缓存,这里不细说。列一下我司的部署方式:


主从部署,app接入代理

这种方式是不太好扩机器。有一种比较好的方式是:一致性哈希算法。 可以参考这篇文章:他山之石中的《一致性哈希算法》

Localcache

在java中,localcache本质上是一个map,对应map中的每一个键值对,可以设置过期时间,也可以通过如LRU(Least Recently Used 最近最少使用)做淘汰策略。

LRU算法实现原理参见他山之石中的《如何设计实现一个LRU Cache》。本质上是一个hashmap+双向链表,每次访问操作都将节点放在链表头部,尾部自然就是最旧的节点了。


LRU算法原理

我对比了两种LocalCache,google的Guava库中的cache模块Ehcache

Guava可以参考文章:他山之石中的《 [Google Guava] 3-缓存》 其基本原理为:ConcurrentMap(利用分段锁降低锁粒度) + LRU算法。

ConcurrentMap分段锁原理参考《Java集合-ConcurrentHashMap原理分析》


ConcurrentMap分段锁

分治思想。

Ehcache相关,可以参考《ehcache基本原理》与《Spring+EhCache缓存实例》。该缓存是一个比较重的localcache。有一个比较有意思的点是:支持磁盘缓存,这样妈妈再也不用担心内存不够用了>->。

我的需求是:可以不用担心内存不足的问题,做一些配置或不需要强一致性数据缓存。甚至可以做伪热加载配置。

因此,我选择使用了Ehcache。如果只是做内存缓存,建议使用guava,有很多有意思的东西,比如缓存失效,自动从数据源加载数据等等。

Ehcache工具类

根据自身需要,封装了一个只走磁盘的无容量限制的localcache工具类,仅供参考,考虑到可以放在多个地方,因此没有走配置文件

package com.fenqile.creditcard.appgatewaysale.provider.util;

import com.alibaba.fastjson.JSONObject;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.config.CacheConfiguration;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.MessageDigest;

/**
 * User: Rudy Tan
 * Date: 2018/3/28
 *
 * 本地缓存工具,基于ehcache,磁盘存储
 *
 * 可以运用于配置文件、接口数据等等,
 *
 */
public class LocalCacheUtil {

    private static final CacheManager cacheManager = CacheManager.create();

    private static Logger LOG = LoggerFactory.getLogger(LocalCacheUtil.class);

    private static String md5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(str.getBytes("utf-8"));

            final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
            StringBuilder ret = new StringBuilder(bytes.length * 2);
            for (int i=0; i<bytes.length; i++) {
                ret.append(HEX_DIGITS[(bytes[i] >> 4) & 0x0f]);
                ret.append(HEX_DIGITS[bytes[i] & 0x0f]);
            }
            return ret.toString();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static Cache getCacheInstance(){
        String cacheKey = "local_cache_"+ md5(Thread.currentThread().getStackTrace()[1].getClassName());
        if (!cacheManager.cacheExists(cacheKey)){
            synchronized (cacheManager){
                if (!cacheManager.cacheExists(cacheKey)){
                    CacheConfiguration cacheConfiguration =  new CacheConfiguration();
                    cacheConfiguration.setTimeToIdleSeconds(60);
                    cacheConfiguration.setTimeToLiveSeconds(60);
                    cacheConfiguration.setName(cacheKey);
                    cacheConfiguration.setMaxEntriesLocalHeap(1);
                    cacheConfiguration.setMaxEntriesLocalDisk(100000);
                    cacheConfiguration.setEternal(false);
                    cacheConfiguration.setOverflowToDisk(true);
                    cacheConfiguration.setMaxElementsInMemory(1);
                    cacheConfiguration.setCopyOnRead(true);
                    cacheConfiguration.setCopyOnWrite(true);

                    Cache cache = new Cache(cacheConfiguration);
                    cacheManager.addCache(cache);
                }
            }
        }
        return cacheManager.getCache(cacheKey);
    }

    private static Element serialization(String key, Object value, Integer expireTime){
        if (StringUtils.isEmpty(key)
                || null == expireTime
                || 0 == expireTime){
            return null;
        }

        String clazz = "";
        String content = "";
        if (null == value){
            clazz = "null";
            content = clazz + "_class&data_null";
        }else {
            clazz = value.getClass().getName();
            content = clazz + "_class&data_"+ JSONObject.toJSONString(value);
        }

        return new Element(key, content, expireTime, expireTime);
    }

    private static Object unSerialization(Element element){
        if (null == element){ return null; }

        String content = (String) element.getObjectValue();
        String[] data = content.split("_class&data_");
        Object response = null;
        try {
            if ("null".equalsIgnoreCase(data[0])){
                return null;
            }
            response = JSONObject.parseObject(data[1], Class.forName(data[0]));
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return response;
    }


    /**
     * 设置本地缓存
     */
    public static boolean setCache(String key, Object value, Integer expireTime){
        Cache cache = getCacheInstance();
        if (null == cache){
            LOG.info("setCache:cache is null, {}, {}, {}", key, value, expireTime);
            return false;
        }

        if (StringUtils.isEmpty(key)
                || null == expireTime
                || 0 == expireTime){
            LOG.info("setCache:params is not ok, {}, {}, {}", key, value, expireTime);
            return false;
        }

        synchronized (cache){
            cache.put(serialization(key, value, expireTime));
            cache.flush();
        }
        return true;
    }

    /**
     * 获取本地缓存
     */
    public static Object getCache(String key){
        Cache cache = getCacheInstance();
        if (null == cache
                || StringUtils.isEmpty(key)){
            LOG.info("getCache:params is not ok, {}", key);
            return null;
        }

        Element element = cache.get(key);
        return unSerialization(element);
    }

    /**
     * 清理本地缓存
     */
    public static boolean delCache(String key){
        Cache cache = getCacheInstance();
        if (null == cache){
            LOG.info("delCache:cache is null, {}", key);
            return true;
        }

        if (StringUtils.isEmpty(key)){
            LOG.info("delCache:params is not ok, {}", key);
            return true;
        }

        synchronized (cache){
            cache.put(serialization(key, null, 0));
            cache.flush();
        }
        return true;
    }
}

good luck.