Guava Cache探索及spring项目整合GuavaCache实例

背景

  对于高频访问可是低频更新的数据咱们通常会作缓存,尤为是在并发量比较高的业务里,原始的手段咱们可使用HashMap或者ConcurrentHashMap来存储.html

  这样没什么毛病,可是会面临一个问题,对于缓存中的数据只有当咱们显示的调用remove方法,才会移除某个元素,即使是高频的数据,也会有访问命中率的高低之分,内存老是有限的,咱们不可能无限地去增长Map中的数据.java

  我但愿的比较完美的场景时.对于一个业务,我只想分配给你2k的内存,咱们假设map中一条数据(键值对)是1B,那么最多它能存2048条数据,当数据达到这个量级的时候,须要淘汰一些访问率比较低的数据来给新的数据腾地方,使用传统的HashMap比较难实现,由于咱们不知道哪些数据访问率低(除非专门去记录),那么Guava针对内存缓存优化的一个组件就闪亮登场了.redis

准备

  上面说到咱们须要一种淘汰策略来自动筛选缓存数据,下面简单了解下,几种淘汰算法算法

  先进先出算法(FIFO):这种淘汰策略顾名思义,先存的先淘汰.这样简单粗暴,可是会错杀一些高频访问的数据spring

  最近最少使用算法(LRU):这个算法能有效优化FIFO的问题,高频访问的数据不太容易被淘汰掉,但也不能彻底避免.GuavaCache一些特性符合这种算法缓存

      最近最少频率算法(LFU): 这个算法又对LRU作了优化,会记录每一个数据的访问次数,综合访问时间和访问次数来淘汰数据.安全

Guava Cache基础

  GuavaCache提供了线程安全的实现机制,简单易用,上手成本很低,在须要使用内存作缓存的业务场景时能够考虑使用.并发

  GuavaCache缓存机制有两个接口,Cache和LoadingCache,后者也是一个接口,继承自Cache,并额外多了几个接口,若是咱们想实例化一个Cache对象,还须要了解一个CacheBuilder类,这个类就是雨历来构建Cache对象的,咱们先来用CacheBuilder实例化一个Cache对象再学习它的一些字段含义.app

public static void main(String[] args) {
        Cache<String,String> myMap = CacheBuilder.newBuilder()
                .expireAfterAccess(30L, TimeUnit.SECONDS)
                .expireAfterWrite(3L,TimeUnit.MINUTES)
                .concurrencyLevel(6)
                .initialCapacity(100)
                .maximumSize(1000)
                .softValues()
                .build();

        myMap.put("name", "张三");

        System.out.println(myMap.getIfPresent("name"));

}

     这样咱们就建立一个相似map接口的Cache对象,描述一下上面建立的这个对象:less

     建立了一个Cache对象,这个对象有这样的特性,初始大小为100(能存100个键值对),最大size为1000,在数据写入3分钟后会被自动移除,而且数据若是在30秒内,没有被访问则会被移除,另外这Map结构的对象支持最多6个调用方同时更新这个缓存结构的数据,即并发更新操做最大数量为6.

  咱们看到还有一个softValues()属性没有讲,会放在下面说明,其实CacheBuilder并不仅有这么几个属性可设置,下面咱们具体讲一下.

CacheBuilder中一些经常使用的属性字段:

  concurrencyLevel(int):指定容许同时更新的操做数,若不设置CacheBuilder默认为4,这个参数会影响缓存存储空间的分块,能够简单理解为,默认会建立指定size个map,每一个map称为一个区块,数据会分别存到每一个map里,咱们根据实际须要设置这个值的大小.

  initialCapacity(int):指定缓存初始化的空间大小,若是设置了40,而且concurrencyLevel取默认,会分红4个区块,每一个区块最大的size为10,当更新数据时,会对这个区块进行加锁,这就是为何说,容许同时更新的操做数为4,延伸一点,在淘汰数据时,也是每一个区块单独维护本身的淘汰策略.也就是说,若是每一个区块size太大,竞争就会很激烈.

  maximumSize(long):指定最大缓存大小.当缓存数据达到最大值时,会按照策略淘汰掉一些不经常使用的数据,须要注意的是,在缓存数据量快要到达最大值的时候,就会开始数据的回收,简单理解为"防患于未然"吧

下边三个参数分别是,SoftValues(),weakKeys(),weakValues(),在解释这三个参数前,须要咱们先了解一下java中的软引用,和弱引用.

  和弱引用对应的是强引用,也是咱们在编码过程当中最常使用的,咱们声明的变量,对象,基本都是强引用,这样的对象,jvm在GC时不会回收,哪怕是抛出OOM.

  而弱引用就不同了,在java中,用java.lang.ref.WeakReference标示声明的值,jvm在垃圾回收的时候会将它回收掉,那么软引用呢?就是用SoftReference标示的,声明为弱引用的对象,会在jvm的内存不足时回收掉.

  看出区别了吗,简单总结下就是,软引用,只有在内存不足时才可能被回收,在正常的垃圾回收时不会被回收,弱引用,会在jvm进行垃圾回收的时候被删除.

  softValues():将缓存中的数据设置为softValues模式。数据使用SoftReference类声明,就是在SoftReference实例中存储真实的数据。设置了softValues()的数据,会被全局垃圾回收管理器托管,按照LRU的原则来按期GC数据。数据被GC后,可能仍然会被size方法计数,可是对其执行read或write方法已经无效

  weakKeys()和weakValues():当设置为weakKey或weakValues时,会使用(==)来匹配key或者value值(默认强引用时,使用的是equals方法),这种状况下,数据可能会被GC,数据被GC后,可能仍然会被size方法计数,可是对其执行read或write方法已经无效

Guava cache在spring项目中的使用

  下面以一个我在项目中的实际应用梳理一下在spring项目中应该若是整合guava cache

 1.引入guava的maven依赖

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>26.0-jre</version>
</dependency>

  上面使用的版本是我在写这篇笔记时的最新版本.

 2.在application-context.xml加入配置

<!--开启缓存注解-->
    <cache:annotation-driven />

    <bean id="cacheManager" class="org.springframework.cache.guava.GuavaCacheManager">
        <property name="cacheSpecification" value="initialCapacity=500,maximumSize=5000,expireAfterAccess=2m,softValues" />
        <property name="cacheNames">
            <list>
                <value>questionCreatedTrack</value>
            </list>
        </property>
    </bean>

  在上面配置中咱们实现了一个cacheManager,这是必需要配置的,默认配置的是org.springframework.cache.support.SimpleCacheManager,咱们这里把它改为了Guava的缓存管理器的实现.若是使用其余的实现,好比redis,这里只须要配置成redis的相关缓存管理器便可

  cacheManager能够简单理解为保存Cache的地方,Cache里边有咱们具体想要缓存的数据,通常以key-value的键值对形式

  上述配置的bean中声明的两个属性,一个是cacheSpecification,不须要多说了,参考上面的详细参数,须要了解一点的是,这里的参数使用的是CacheBuilderSpec类,以解析表明CacheBuilder配置的字符串的形式来建立CacheBuilder实例

  cacheNames能够根据本身的实际业务命名,可声明多个

 3.在代码中使用spring的cache相关注解

@Cacheable(value = "questionCreatedTrack",key="#voiceId",condition = "#voiceId>0")
    public Long getQuestionIdByVoiceId(Long anchorId, Long voiceId) {
        String key = String.format(HOMEWORK_QUESTION_ANCHOR_KEY, anchorId);
        String value = redisProxy.getValue(key, String.valueOf(voiceId));
        return StringUtils.isEmpty(value) ? null : Long.parseLong(value);
    }

    @CachePut(value = "questionCreatedTrack",key = "#voiceId",condition = "#voiceId>0")
    public Long statCollectionQuestionToCache(Long anchorId, Long voiceId, Long questionId) {
        String key = String.format(HOMEWORK_QUESTION_ANCHOR_KEY, anchorId);
        redisProxy.setOneToHash(key, String.valueOf(voiceId), String.valueOf(questionId));
        return questionId;
    }

    @CacheEvict(value = "questionCreatedTrack",key="#voiceId")
    public void removeCollectionQuestionFromCache(Long anchorId, Long voiceId) {
        String key = String.format(HOMEWORK_QUESTION_ANCHOR_KEY, anchorId);
        redisProxy.deleteOneToHash(key, String.valueOf(voiceId));
    }

 先简单说一下这里的逻辑,我主要是使用内存作一个一级缓存,redis作第二级的缓存,上面三个方法的做用分别是

  getQuestionIdByVoiceId(..):经过voiceId查询questionId,使用@Cacheable注解标记的意思是,代码执行到这个方法时,会先去guava cache里去找,有的话直接返回不走方法,没有的话再去执行方法,返回后同时加入Cache,缓存结构中的value是方法的返回值,key是方法入参中的vocieId,这里的缓存结构是,key=voiceId,value=questionId

  statCollectionQuestionToCache():方法的逻辑是将voiceId和questionId保存进redis里,使用@CachePut注解标记的意思是,不去缓存里找,直接执行方法,执行完方法后,将键值对加入Cache.

  removeCollectionQuestionFromCache():方法的逻辑是删除redis中的key为voiceId的数据,使用@CacheEvict注解标记的意思是,清除Cache中key为voiceId的数据

  经过以上三个注解,能够实现这样的功能,当查询voiceId=123的对应questionId时,会先去Cache里查,Cache里若是没有再去redis里查,有的话同时加入Cache,(没有的话,也会加入Cache,这个下面会说),而后在新增数据以及移除数据的时候,redis和Cache都会同步.

  @Cacheable方法查询结果为null怎么处理

  这个须要咱们根据实际须要,决定要不要缓存查询结果为null的数据,若是不须要,须要使用下面的注解

    @Cacheable(value = "questionCreatedTrack",key="#voiceId",condition = "#voiceId>0",unless = "#result==null")

参考资料

http://www.voidcn.com/article/p-pvvfgdga-bos.html

http://www.javashuo.com/article/p-yfruwgfl-dm.html

相关文章
相关标签/搜索