daily 2017 7 19 本地缓存 - wtdig/study GitHub Wiki

本地缓存综述

java中的本地缓存,工作后陆续用到,一直想写,一直无从下手,最近又涉及到这方面的问题了,梳理了一下。自己构造单例、guava、ehcache基本上涵盖了目前的大多数行为了。

为什么要有本地缓存?

在系统中,有些数据,数据量小,但是访问十分频繁(例如国家标准行政区域数据),针对这种场景,需要将数据搞到应用的本地缓存中,以提升系统的访问效率,减少无谓的数据库访问(数据库访问占用数据库连接,同时网络消耗比较大),但是有一点需要注意,就是缓存的占用空间以及缓存的失效策略。

为什么是本地缓存,而不是分布式的集群缓存?

目前的数据,大多是业务无关的小数据缓存,没有必要搞分布式的集群缓存,目前涉及到订单和商品的数据,会直接走DB进行请求,再加上分布式缓存的构建,集群维护成本比较高,不太适合紧急的业务项目。

这里介绍一下缓存使用的三个阶段(摘自info架构师文档)

本地缓存在那个区域?

目前考虑的是占用了JVM的heap区域,再细化一点的就是heap中的old区,目前的数据量来看,都是一些小数据,加起来没有几百兆,放在heap区域最快最方便。后期如果需要放置在本地缓存的数据大的时候,可以考虑在off-heap区域,但是off-heap区域的话,需要考虑对象的序列化(因为off-heap区域存储的是二进制的数据),另外一个的话就是off-heap的GC问题。其实,如果真的数据量比较大,那其实就可以考虑搞一个集中式的缓存系统,可以是单机,也可以是集群,来承担缓存的作用。

搞一个单例模式,里面有个Map的变量来放置数据

关于单例模式,一个既简单又复杂的模式(http://iamzhongyong.iteye.com/blog/1539642) 非常典型的代码如下:

public class SingletonMap { //一个本地的缓存Map private Map localCacheStore = new HashMap();
//一个私有的对象,非懒汉模式
private static SingletonMap singletonMap = new SingletonMap(); 

//私有构造方法,外部不可以new一个对象
private SingletonMap(){
}  

//静态方法,外部获得实例对象
public static SingletonMap getInstance(){
    return singletonMap;
}

//获得缓存中的数据
public Object getValueByKey(String key){
    return localCacheStore.get(key);
}
//向缓存中添加数据
public void putValue(String key , Object value){
    localCacheStore.put(key, value);
}

} 这种能不能用?可以用,但是非常局限

但是这种的就是本地缓存了吗?答案显然不是,为啥呢?

1、 没有缓存大小的设置,无法限定缓存体的大小以及存储数据的限制(max size limit);

2、 没有缓存的失效策略(eviction policies);

3、 没有弱键引用,在内存占用吃紧的情况下,JVM是无法回收的(weak rererences keys);

4、 没有监控统计(statistics);

5、 持久性存储(persistent store);

所以,这种就直接废掉了。。。

引入EhCache来构建缓存(详细介绍: http://raychase.iteye.com/blog/1545906) EhCahce的核心类:

A、CacheManager:Cache的管理类;

B、Cache:具体的cache类信息,负责缓存的get和put等操作

C、CacheConfiguration :cache的配置信息,包含策略、最大值等信息

D、Element:cache中单条缓存数据的单位

典型的代码如下:

public static void main(String[] args) { //EhCache的缓存,是通过CacheManager来进行管理的 CacheManager cacheManager = CacheManager.getInstance();

    //缓存的配置,也可以通过xml文件进行
    CacheConfiguration conf = new CacheConfiguration();
    conf.name("cache_name_default");//设置名字
    conf.maxEntriesLocalHeap(1000);//最大的缓存数量
    conf.memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LRU);//设置失效策略
     
    //创建一个缓存对象,并把设置的信息传入进去
    Cache localCache = new Cache(conf);
     
    //将缓存对象添加到管理器中
    cacheManager.addCache(localCache);
             
    localCache.put(new Element("iamzhongyong", new Date()));
     
    System.out.println(localCache.getSize());
    System.out.println(localCache.getStatistics().toString());
    System.out.println(localCache.getName());
    System.out.println(localCache.get("iamzhongyong").toString());
    System.out.println(localCache.get("iamzhongyong").getObjectValue());   
}

当然,Cache的配置信息,可以通过配置文件制定了。。。

优点:功能强大,有失效策略、最大数量设置等,缓存的持久化只有企业版才有,组件的缓存同步,可以通过jgroup来实现

缺点:功能强大的同时,也使其更加复杂

引入guava的cacheBuilder来构建缓存

这个非常强大、简单,通过一个CacheBuilder类就可以满足需求。

缺点就是如果要组件同步的话,需要自己实现这个功能。

典型的代码如下:

public class GuavaCacheBuilderTest { public static void main(String[] args) throws Exception{ GuavaCacheBuilderTest cache = new GuavaCacheBuilderTest(); cache.getNameLoadingCache("bixiao"); } public void getNameLoadingCache(String name) throws Exception{ LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(20)//设置大小,条目数
.expireAfterWrite(20, TimeUnit.SECONDS)//设置失效时间,创建时间
.expireAfterAccess(20, TimeUnit.HOURS) //设置时效时间,最后一次被访问
.removalListener(new RemovalListener<String, String>() { //移除缓存的监听器 public void onRemoval(RemovalNotification<String, String> notification) { System.out.println("有缓存数据被移除了"); }}) .build(new CacheLoader<String, String>(){ //通过回调加载缓存 @Override public String load(String name) throws Exception { return name + "-" + "iamzhongyong"; } }); System.out.println(cache.get(name)); //cache.invalidateAll(); } }

缓存预热怎么搞?

A、全量预热,固定的时间段移除所有,然后再全量预热

适用场景:

1、数据更新不频繁,例如每天晚上3点更新即可的需求;

2、数据基本没有变化,例如全国区域性数据;

B、增量预热(缓存查询,没有,则查询数据库,有则放入缓存)

适用场景:

1、 数据更新要求缓存中同步更新的场景

​集群内部,缓存的一致性如何保证?

如果采用ehcache的话,可以使用框架本身的JGroup来实现组内机器之间的缓存同步。

如果是采用google的cacheBuilder的话,需要自己实现缓存的同步。

A、非实时生效数据:数据的更新不会时时发生,应用启动的时候更新即可,然后定时程序定时去清理缓存;

B、需要实时生效数据:启动时可预热也可不预热,但是缓存数据变更后,集群之间需要同步


Guava Cache本地缓存

适用性

  

缓存在很多情况下非常实用。例如,计算或检索一个值的代价很高,并且对同样的输入需要不止一次获取值的时候,就应当考虑使用缓存。

  

Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所添加的元素,直到显式的移除;Guava Cache为了限制内存的占用,通常都是设定为自动回收元素。在某些场景下,尽管LoadingCahe不回收元素,但是它还是很有用的,因为它会自动加载缓存。

  

Guava Cache适用场景:

你愿意消耗一部分内存来提升速度;

你已经预料某些值会被多次调用;

缓存数据不会超过内存总量;

 

Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制。整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好。

创建方式

  

CacheLoader和Callable通过这两种方法创建的cache,和通常用map来缓存的做法比,不同在于这两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值。

  

但不同的在于cacheloader的定义比较宽泛, 是针对整个cache定义的,可以认为是统一的根据key值load value的方法。而callable的方式较为灵活,允许你在get的时候指定。

  

1、CacheLoader

public class AppkeyInfoLocalCache {

private static Logger log = Logger.getLogger(AppkeyInfoLocalCache.class);

static LoadingCache<String, AppkeyInfoBasic> cache = CacheBuilder.newBuilder().refreshAfterWrite(3, TimeUnit.HOURS)// 给定时间内没有被读/写访问,则回收。
        .expireAfterAccess(APIConstants.TOKEN_VALID_TIME, TimeUnit.HOURS)// 缓存过期时间和redis缓存时长一样
        .maximumSize(1000).// 设置缓存个数
        build(new CacheLoader<String, AppkeyInfoBasic>() {
            @Override
            /** 当本地缓存命没有中时,调用load方法获取结果并将结果缓存 **/
            public AppkeyInfoBasic load(String appKey) throws DaoException {
                return getAppkeyInfo(appKey);
            }

            /** 数据库进行查询 **/
            private AppkeyInfoBasic getAppkeyInfo(String appKey) throws DaoException {
                log.info("method<getAppkeyInfo> get AppkeyInfo form DB appkey<" + appKey + ">");
                return ((AppkeyInfoMapper) SpringContextHolder.getBean("appkeyInfoMapper"))
                        .selectAppkeyInfoByAppKey(appKey);
            }
        });

public static AppkeyInfoBasic getAppkeyInfoByAppkey(String appKey) throws DaoException, ExecutionException {
    log.info("method<getAppkeyInfoByAppkey> appkey<" + appKey + ">");
    return cache.get(appKey);
}

}

  

2、Callable

public void testcallableCache()throws Exception{ Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).build();
String resultVal = cache.get("jerry", new Callable() {
public String call() {
String strProValue="hello "+"jerry"+"!";
return strProValue; }
});
System.out.println("jerry value : " + resultVal);

    resultVal = cache.get("peida", new Callable<String>() {  
        public String call() {  
            String strProValue="hello "+"peida"+"!";                
            return strProValue;
        }  
    });  
    System.out.println("peida value : " + resultVal);  
}
本地缓存案例:

private Cache<String, String> localCache = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();


    public String getValueByKey(String key){
        try {
            return localCache.get(key, () -> {
                //从数据库获取数据
                String valueFromDb = "dbData";
                if(StringUtil.isBlank(valueFromDb)){
                    return "";
                }
                return valueFromDb;
            });
        } catch (ExecutionException e) {
            e.printStackTrace();
            return null;
        }
    }
⚠️ **GitHub.com Fallback** ⚠️