学习Guava Cache知识汇总

 (一)MapMaker

在软件开发中,Cache缓存技术一直是非常重要的主题,不管我们正在进行任何简单的编程工作,我们总能在编程过程中找到一些缓存机制,即使是你使用一个 Map获取静态的值,它也是缓存的一种形式,但我们大多数人并不清楚其中的缓存使用。Guava Cache为我们提供了相比于简单的HashMap更强大和灵活的缓存机制,并不像EhcacheMemcache那样具有健壮性。在本系列的学习中,我们将学习Guava提供的缓存机制,首先我们学习Guava Cache(一)MapMaker

我们可以在com.google.common.collect包下面找到MapMaker类,那么问题来了,我们为什么不在Guava Collections系列学习,尽管我们可能已经在Collections的学习中提到了MapMaker,本篇中,我们将MapMaker作为一个提供最基本缓存功能的类进行学习,MapMaker类使用了流畅的接口API,允许我们快速的构造ConcurrentHashMap,我们来看下面的例子:

ConcurrentMap books = new

        MapMaker().concurrencyLevel(2)

        .softValues()

        .makeMap();

上面的例子中,我们构造了一个ConcurrentHashMap,使用String类型作为key,使用Book对象作为value值,通过对 ConcurrentHashMap声明的泛型进行指定,我们首先调用了concurrencyLevel()方法,设置了我们允许在map中并发修改的数量,我们还指定了softValues()方法,这样map中的value值都包裹在一个SoftReference(软引用)对象中,可以在内存过低的时候被当作垃圾回收。

其他我们可以指定的方法还包括:weakKeys()weakValues(),但是MapMaker没有提供softKeys(),当我们给keys values使用WeakReferences(弱引用)SoftReference(软引用)时,如果键值的其中一个被当做垃圾回收,整个键值对就会从map中移除,剩余的部分并不会暴露给客户端。

最后值得注意的一点:MapMaker中的softValues()方法在最近的几个guava版本中,已经被标注为 DeprecatedMapMaker中的缓存机制已经被移动到com.google.common.cache.CacheBuilder 中,MapMaker中的softValues()方法也已经被替换为com.google.common.cache.CacheBuilder#softValuesCacheBuilder的实现是来自MapMaker分支的一个简单增强版API

 

 (二)Guava caches

在开始Guava CacheBuilder的学习之前,我们先来做一些准备工作:

Guava Cache缓存机制有两个基本的接口:CacheLoadingCacheLoadingCache接口继承自Cache接口,本篇我们首先来学习Cache接口。

Cache

Cache接口提供了键和值的映射,但是Cache接口中提供的一些方法比HashMap提供的更基本。使用mapscaches的传统做法是:我们提供一个key,如果缓存中存在key对应的value值,我们就将这个value值返回,否则的话,如果通过相应的key找到映射关系,就返回null 值,为了在缓存中设置value值,我们可能需要这样做:

put(key,value);

我们需要明确的在cachemap中关联keyvalueCache使用了传统的put方法设置value值,但是获取value的时候,Guava Cache有自己的调用风格,如下:

V value = cache.get(key, Callable value);

上面的方法会检索当前的value值,如果值不存在的话会从Callback实例中提取value值,通过key关联value,并返回相应的value值,它为我们提供了通过调用一个方法来替代下面风格的编程:

value = cache.get(key);

if(value == null){

    value =someService.retrieveValue();

    cache.put(key,value);

}

一个回调函数的使用意味着一个异步操作可能会发生,但是如果我们不需要执行一个异步任务我们又该怎么做呢?我们可以使用com.google.common.util. concurrent包里面的Callables类,Callables提供了一个方法用于处理Callable接口,使用代码如下所示:

Callable value = Callables.returning( "Foo" );

在上面的代码中,我们调用returning()方法构造并返回了一个Callable实例,当这个Callable实例调用get方法时会返回我们传递的“Foo”值,所以我们可以重新实现一下我们最初的代码:

cache.get(key,Callables.returning(someService.retrieveValue());

需要记住的是,如果value存在,缓存的value就会被返回。如果我们偏爱如果存在...否则...”这样的编程风格的话,Guava同样提供了 getIfPresent(key)方法,使我们可以以传统方式编程,同样也一些使缓存中的value值失效的方法,如下所示:

invalidate(key):废弃缓存中当前key对应的所有value值。

invalidateAll():废弃缓存中所有的value值。

invalidateAll(Iterablekeys):废弃传入key集合中对应的所有缓存中的value值。

 

LoadingCache接口扩展了Cache接口的自动装载的功能,考虑下面的代码实例:

Book book = loadingCache.get(id);

在上面的代码中,如果book对象在get方法调用执行的时候不可用,LoadingCache知道怎样返回对象,保存在缓存中,然后返回value值。

Loading values

由于LoadingCache的实现是线程安全的,通过同样的key值调用get方法,当缓存还在装载的时候就会阻塞线程。一旦value值被加载,该调用将返回最原始的调用get方法返回的value值,然而,多个通过不同key值的调用则会并发的加载,如果我们拥有一个键值的集合,并且想要去检索每个键对应的value值,我们需要像下面这样调用:

ImmutableMap map = cache.getAll(Iterable);

如上所示,getAll方法返回了一个不可变的Map集合,通过指定键值以及与键值关联的缓存中的value值,getAll返回的map可能是所有缓存的value值,也可能是新检索到的value值,或者是已有缓存和新检索到的value值的混合。

Refreshingvalues in the cache

LoadingCache 同样提供了一种缓存中更新value值的机制:

refresh(key);

通过调用refresh方法,LoadingCache会检索到对应key新的value值,当前的value值在新的value值返回前不会被从缓存出废弃,这意味着如果在加载的过程中调用get方法,则会返回缓存中当前的value值。如果在refresh方法调用中出现异常,原始的value值会依然在缓存中保存。需要记住的是:如果value值被异步的检索,在value值被真正的更新前,方法都将会返回原始的value值。

 

 (三)CacheBuilder

我们了解了Cache LoadingCache,它们是Guava Cache缓存机制的基础接口,我们本篇即将学习的CacheBuilder类,通过建造者(Builder)模式为我们提供了一种获取Cache LoadingCache实例的方式,接下来,我们就开始Guava Cache系列:CacheBuilder的学习。

CacheBuilder提供了许多选项,我们可以选择部分来创建Cache实例,不需要列出所有的选项,这是Builder模式的魅力,接下来,我们通过下面的一些例子,来了解如何在guava 中使用Cache缓存机制,第一个例子演示的是在缓存条目加载到缓存中后,我们如何指定条目失效:       

 LoadingCache tradeAccountCache =

                CacheBuilder.newBuilder()

                        .expireAfterWrite(5L, TimeUnit.MINUTES)

                        .maximumSize(5000L)

                        .removalListener(new TradeAccountRemovalListener())

                        .ticker(Ticker.systemTicker())

                        .build(new CacheLoader() {

                            @Override

                            public TradeAccount load(String key)throws

                                    Exception {

                                return

                                       tradeAccountService.getTradeAccountById(key);

                            }

                        });

上面的代码中,我们为如下所示的TradeAccount对象构造了一个LoadingCache实例:

    publicclassTradeAccount {

        privateString id; //ID

        privateString owner; //所有者

        privatedouble balance; //余额

    }

我们来简要的对第一个例子做一些说明:

1.     首先,我们调用了expireAfterWrite方法,它可以自动的使缓存条目在指定的时间后失效,在本例中,是5分钟。

2.     第二步,我们通过maximumSize方法,5000作为传入值,指定了缓存的最大大小,当缓存的大小逼近到最大值时,缓存中一些最近很少使用到的条目将会被移除,不一定在缓存大小达到最大值甚至超过最大值才移除。

3.     我们注册了一个RemovalListener监听器实例,它可以在缓存中的条目被移除后接收通知,更多RemovalListener,我们将在Guava库学习:学习Guava Cache(七)中进行学习,敬请关注。

4.     我们添加了一个Ticker实例,通过调用ticker方法,此方法提供了缓存条目过期的时间,纳秒级的精密度。

5.     最后,我们调用了build方法,传入了一个新的CacheLoader实例,当缓存中的key存在,value不存在时,这个实例将被用来重新获取TradeAccount对象。

在接下来的实例中,我们来看看怎么使缓存条目失效,基于上一次条目被访问后所经过的时间:

LoadingCache bookCache = CacheBuilder.newBuilder()

        .expireAfterAccess(20L, TimeUnit.MINUTES)

        .softValues()

        .removalListener(new BookRemovalListener())

        .build(newCacheLoader() {

            @Override

            publicBook load(String key)throws Exception {

                returnbookService.getBookByIsbn(key);

            }

        });

在这个例子中,我们对代码做了轻微的调整,我们来进行一下说明:

1.     通过调用expireAfterAccess方法,我们指定了缓存中的条目在上一次访问经过20分钟后失效。

2.     我们不再明确的指定缓存的最大值,而是通过调用softValues()方法,JVM虚拟机将缓存中的条目包装成软引用对象,以限制的缓存大小。如果内存空间不足,缓存中的条目将会被移除。需要注意的是,哪些软引用可以被垃圾回收是由JVM内部进行的LRU计算所决定的。

3.     最后,我们添加了一个类似的RemovalListener监听器,用于处理缓存中value值不存在的条目。

来看最后一个例子,我们将介绍如何自动的刷新LongCache缓存中的条目值:

LoadingCache tradeAccountCache =

        CacheBuilder.newBuilder()

                .concurrencyLevel(10)

                .refreshAfterWrite(5L, TimeUnit.SECONDS)

                .ticker(Ticker.systemTicker())

                .build(newCacheLoader() {

                    @Override

                    publicTradeAccount load(String key)

                            throws Exception {

                        return

                               tradeAccountService.getTradeAccountById(key);

                    }

                });

在上面的例子中,我们再次做了细微的调整,说明如下:

1.     通过调用concurrencyLevel方法,我们设置了并发更新操作的数量为10,如果不设置的话,默认值为4

2.     不再明确的移除缓存条目,而是通过refreshAfterWrite方法在给定的时间过去后,刷新缓存中的条目值,当缓存条目的值被调用并且已经超过了设置的时间,刷新缓存的触发器将处于活动状态。

3.     我们添加了纳秒级别的Ticker,以刷新那些符合条件的条目值。

4.     最后,通过调用build方法,我们指定了需要使用的Loader

代码地址:http://git.codeweblog.com/realfighter/xx566-diary/blob/master/src/guava/CacheBuilderTest.java

这里推荐一篇文章,对CacheBuilder的机制有比较深入的讲解:Guava缓存器源码分析——CacheBuilder,可以学习下。

 

 (四)CacheBuilderSpec

我们学习并了解了使用CacheBuilder方便的构造CacheLoadingCache实例,我们可以通过CacheBuilder提供的 newBuilder()方法,使用建造者模式构建CacheBuilder实例,另外Guava Cache提供了CacheBuilderSpec类创建CacheBuilder实例,接下来,我们就来开始Guava CacheCacheBuilderSpec的学习。

CacheBuilderSpec类以解析代表CacheBuilder配置的字符串的形式来创建CacheBuilder实例,需要说明的是,Guava没有处理编译时异常,这将会导致当传入的字符串无效时,会出现编译时异常,下面是一个可以用来创建CacheBuilderSpec实例的有效字符串:

//配置CacheBuilder的字符串

String configString = "concurrencyLevel=10,refreshAfterWrite=5s";

通过上面的字符串,我们可以创建一个与(三)CacheBuilder 最后一个例子相同的CacheBuilder实例,通过一些方法来指定时间(refreshAfterWriteexpireAfterAccess等等),间隔的整数后面的‘s’,‘m’‘h’‘d’对应于秒,分钟,小时或天数,这里没有毫秒甚至纳秒级别的设置,当我们指定好了配置的字符串,我们可以通过下面的方式创建一个CacheBuilderSpec实例:

//解析字符串,创建CacheBuilderSpec实例

CacheBuilderSpecspec = CacheBuilderSpec.parse(configString);

我们可以通过下面的方式,使用CacheBuilderSpec实例创建CacheBuilder实例:

//通过CacheBuilderSpec实例构造CacheBuilder实例

CacheBuilder.from(spec);

这里,我们调用了CacheBuilder中的静态from方法,使用CacheBuilderSpec 对象构造了CacheBuilder实例,通过格式化的字符串设置了CacheBuilder的属性,我们可以使用返回的CacheBuilder实例,像我们之前一样调用一些适当的方法,比如添加一个RemovalListener或者通过CacheBuilder创建一个LoadingCache例:

//配置CacheBuilder的字符串

String spec = "concurrencyLevel=10,expireAfterAccess=5m,softValues";

//解析字符串,创建CacheBuilderSpec实例

CacheBuilderSpeccacheBuilderSpec = CacheBuilderSpec.parse(spec);

//通过CacheBuilderSpec实例构造CacheBuilder实例

CacheBuildercacheBuilder = CacheBuilder.from(cacheBuilderSpec);

//ticker:设置缓存条目过期时间

//removalListener:监听缓存条目的移除

cacheBuilder.ticker(Ticker.systemTicker())

        .removalListener(new TradeAccountRemovalListener())

        .build(newCacheLoader() {

            @Override

            public TradeAccount load(String key)throws

                    Exception {

                return

                       tradeAccountService.getTradeAccountById(key);

            }

        });

上面的例子中,我们注册了一个Ticker实例和RemovalListener实例,通过build方法指定了一个CacheLoader,以字符串方式使用CacheBuilderSpec用于示例的演示目的,通常情况下,这个字符串将从命令行输入,或来自于配置文件的读取。

 

代码地址: http://git.codeweblog.com/realfighter/xx566-diary/blob/master/src/guava/CacheBuilderSpecTest.java

 

 (五)CacheLoader

CacheLoader是一个抽象类,因为其中的load方法是一个抽象方法,翻看源码,其中也提供了一个loadAll方法,用于接收一个 Iterable对象,loadAll方法代表加载所有包含在传入Iterable中的缓存条目(除非我们覆盖loadAll法),CacheLoader中提供了两个静态方法,能够使我们利用之前Guava函数式编程中学习到的一些工具类,如FunctionSupplier

第一个方法接收一个Function对象,用于进行对象传入到传出的转换

CacheLoadervalue> cacheLoader =

        CacheLoader.from(Functionfunc);

Function对象作为CacheLoader.from方法的参数时,我们可以获取到返回的CacheLoader实例,key为传入的对象,valuekey经过Function转换后的值,下面是一个简单的示例:

@Test

public void testCacheLoaderFunction() throws Exception {

    Function function = newFunction() {

        @Override

        public Stringapply(Date input) {

            returnnew SimpleDateFormat("yyyy-MM-dd").format(input);

        }

    };

    CacheLoader cacheLoader = CacheLoader.from(function);

 

    assertThat(cacheLoader.load(new Date()), is("2014-12-06"));

 

}

与之类似的,CacheLoader.from方法也可以接收一个Supplier,进行对象的包装构建:

CacheLoader<Object,Value> cacheLoader =

        CacheLoader.from(Supplier<Value> supplier);

上面的方法,我们通过一个Supplier方法构建了一个CacheLoader实例,值得注意的是,任意传递给CacheLoader的键,都会导致Supplierget方法被调用。下面是一个简单的示例:

@Test

     publicvoidtestCacheLoaderSupplier() throws Exception {

    Supplier supplier = new Supplier() {

        @Override

        publicLong get() {

            //这里简单的返回时间戳

            returnSystem.currentTimeMillis();

        }

    };

    CacheLoader cacheLoader= CacheLoader.from(supplier);

    System.out.println(cacheLoader.load("first"));

    Thread.sleep(300);//这里线程休息下,方便测试

    System.out.println(cacheLoader.load("second"));

}

代码地址:http://git.codeweblog.com/realfighter/xx566-diary/blob/master/src/guava/CacheLoaderTest.java

 

 (六)CacheStats

Guava Cache提供了一种非常简便的方式,用于收集缓存执行的统计信息,需要注意的是,跟踪缓存操作将会带来性能的损失,想要收集缓存的信息,我们只需要在使用CacheBuilder的时候声明我们想要收集统计信息即可:

LoadingCache<String,TradeAccount> tradeAccountCache =  CacheBuilder.newBuilder() .recordStats()

上面的代码,我们通过建造者模式构造了一个LoadingCache实例,想要启用缓存信息的统计,我们唯一要做的就是在builder里面通过recordStats()注册,而想要获取统计的信息,我们只需要通过CacheLoadingCache调用stats()方法,就将返回一个CacheStats实例,通过CacheStats实例可以获取到需要的统计信息,来看接下来的例子:

CacheStats cacheStats = cache.stats();

下面是一个概述的清单,我们可以通过CacheStats获取的一些信息:

1.     加载缓存条目值所耗费的平均时间;

2.     请求的缓存条目的命中率;

3.     请求的缓存条目的未命中率;

4.     缓存条数被移除的数量;

涉及缓存性能的还有许多的信息,上面的清单只是一些我们通过CacheStats获取到的一些信息,最后,我们翻开CacheStats的源码,整理一下其中提供的公共方法,如下:

requestCount():返回Cachelookup方法查找缓存的次数,不论查找的值是否被缓存。

hitCount():返回Cachelookup方法命中缓存的次数。

hitRate():返回缓存请求的命中率,命中次数除以请求次数。

missCount():返回缓存请求的未命中的次数。

missRate():返回缓存请求未命中的比率,未命中次数除以请求次数。

loadCount():返回缓存调用load方法加载新值的次数。

loadSuccessCount():返回缓存加载新值的成功次数。

loadExceptionCount():返回缓存加载新值出现异常的次数。

loadExceptionRate():返回缓存加载新值出现异常的比率。

totalLoadTime():返回缓存加载新值所耗费的总时间。

averageLoadPenalty():缓存加载新值的耗费的平均时间,加载的次数除以加载的总时间。

evictionCount():返回缓存中条目被移除的次数。

minus(CacheStatsother):返回一个新的表示当前CacheStats与传入CacheStats之间差异的CacheStats实例。

plus(CacheStatsother):返回一个新的表示当前CacheStats与传入CacheStats之间总计的CacheStats实例。

 

 (七)RemovalListener

接下来,我们详细的介绍RemovalListener_RemovalNotification

就像名字所表名的,当从缓存中移除一个条目时,RemovalListener会被通知,与Java中的大多数Listener类似,RemovalListener被设计成了一个接口,且有一个onRemoval()方法,可以接收一个RemovalNotification对象,RemovalListener参数化如下:

RemovalListener

 

其中,K表示我们想要去监听的键的类型,V表示当缓存条目移除时需要被通知的值的类型,如果我们想要知道任何被移除的条目,可以简单的使用Object做为K V的类型。

 

RemovalNotification

RemovalNotification表示RemovalLitener对象删除一个条目时接收信号的实例对象,RemovalNotification类实现了Map.Entry接口,因此,我们可以访问缓存条目中实际的键和值对象,我们需要注意的是,当由于垃圾回收机制条目被移除时,那些value值可能为null

通过调用RemovalListener实例的getCause()方法,我们可以确定条目被移除的原因,它将会返回一个RemovalCause的枚举,枚举的可能值如下所示:

·       COLLECTED:表明键或值被垃圾回收。

·       EXPIRED:表明最近一次写条目或获取条目的时间超时。

·       EXPLICIT:表明用户手动的移除条目。

·       REPLACED:表明条目不是真正的被移除,只是value值被改变。

·       SIZE:表明由于Cache的长度达到或接近设置的最大限制,条目被移除。

当条目被移除时,如果我们需要执行任何的操作,最好是异步的进行

 

RemovalListenersRemovalListeners能够帮助我们异步的处理,缓存条目移除后的通知,为了让我们的Listener可以在条目被移除时异步的处理通知,我们可以像下面这样调用RemovalListeners. asynchronous方法:


    private ExecutorService executor;

    private ListeningExecutorService executorService;

 

    @Before

    publicvoidsetUp(){

        executor = Executors.newCachedThreadPool();

        executorService = MoreExecutors.listeningDecorator(executor);

    }

 
    @Test
    publicvoidtestRemovalListener(){
        RemovalListener myRemovalListener = new
                RemovalListener() {
                    @Override

                    publicvoidonRemoval(RemovalNotification notification){
                        //Do something here
                    }
                };

        RemovalListener removalListener =

                RemovalListeners.asynchronous(myRemovalListener, executorService);

    }

 

上面,我们通过构造好的RemovalListenerexecutorService实例,并被它们作为参数传入RemovalListeners.asynchronous的方法中。这个方法返回了一个RemovalListener实例,将异步的处理移除条目后的通知,这个步骤会发生在我们将这个RemovalListener注册到CacheBuilder实例后。

 

最近在做性能测试,突然发现内存溢出,根据现象发现是cache调用有问题,所以了解一下cache的命中率。参考了这几篇文章,为了以后方便,则自己作为笔记记录博客。原文详情请参考:

http://www.codeweblog.com/guava%E5%BA%93%E5%AD%A6%E4%B9%A0-%E5%AD%A6%E4%B9%A0guava-cache%E7%9F%A5%E8%AF%86%E6%B1%87%E6%80%BB/

 

你可能感兴趣的:(学习Guava Cache知识汇总)