当项目中需要使用local cache的时候,一般都会通过基于 ConcurrentHashMap或者LinkedHashMap来实现自己的LRU Cache。在造轮子过程中,一般都需要解决一下问题:
1. 内存是有限了,所以需要限定缓存的最大容量.
2. 如何清除“太旧”的缓存entry.
3. 如何应对并发读写.
4.缓存数据透明化:命中率、失效率等.
cache的优劣基本取决于如何优雅高效地解决上面这些问题。Guava cache很好地解决了这些问题,是一个非常好的本地缓存,线程安全,功能齐全,简单易用,性能好。整体上来说Guava cache 是本地缓存的不二之选。
下面是一个简单地例子:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES) .removalListener(MY_LISTENER) .build(new CacheLoader<Key, Graph>() { public Graph load(Key key) throws AnyException { return createExpensiveGraph(key); } });复制代码
接下来,我们从多个角度来介绍如何使用Guava cache。
一、创建cache:
一般来说,在工作中我们一般这样使用remote cache或者local cache:
User user = cache.get(usernick); if(user == null){ user = userDao.getUser(usernick); cache.put(usernick, user);
} return user;
即if cached, return; otherwise create/load/compute, cache and return。
而Guava cache 通过下面两种方式以一种更优雅的方式实现了这个逻辑:
1. From A CacheLoader
2. From A Callable
通过这两种方法创建的cache,和普通的用map缓存相比,不同在于,都实现了上面提到的——“if cached, return; otherwise create/load/compute, cache and return”。但不同的在于cacheloader的定义比较宽泛,是针对整个cache定义的,可以认为是统一的根据key值load value的方法。而callable的方式较为灵活,允许你在get的时候指定。举两个栗子来介绍如何使用这两种方式
From CacheLoader:
LoadingCache<String, String> graphs = CacheBuilder.newBuilder().maximumSize(1000) .build(new CacheLoader<String, String>() { public String load(String key) { // 这里是key根据实际去取值的方法,例如根据这个key去数据库或者通过复杂耗时的计算得出 System.out.println("no cache,load from db"); return "123"; } }); String val1 = graphs.get("key"); System.out.println("1 value is: " + val1); String val2 = graphs.get("key"); System.out.println("2 value is: " + val2);From Callable: Cache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(1000).build(); String val1 = cache.get("key", new Callable<String>() { public String call() { // 这里是key根据实际去取值的方法,例如根据这个key去数据库或者通过复杂耗时的计算得出 System.out.println("val call method is invoked"); return "123"; } }); System.out.println("1 value is: " + val1); String val2 = cache.get("testKey", new Callable<String>() { public String call() { // 这里是key根据实际去取值的方法,例如根据这个key去数据库或者通过复杂耗时的计算得出 System.out.println("val call method is invoked"); return "123"; } }); System.out.println("2 value is: " + val2);复制代码
需要注意的是,所有的Guava caches,不论是否是loader模式,都支持get(Key,Callable<V>)方法。
另外,除了上述这两种方式来更新缓存外,Guava cache当然也支持Inserted Directly:Values也可以通过cache.put(key,value)直接将值插入到cache中。该方法将覆盖key对应的entry。
二、缓存移除
内存是有限,所以不能把所有的东西都加载到内存中,过大的local cache对任何java应用来说都是噩梦。因此local cache必须提供不同的机制来清除“不必要”的缓存entry,平衡内存使用率和命中率。Guava Cache提供了3中缓存清除策略:size-based eviction, time-based eviction, and reference-based eviction.
size-based eviction:基于cache容量的移除。如果你的cache不允许扩容,即不允许超过设定的最大值,那么使用CacheBuilder.maxmuSize(long)即可。在这种条件下,cache会自己释放掉那些最近没有或者不经常使用的entries内存。这里需要注意一下两点:
1.并不是在超过限定时才会删除掉那些entries,而是在即将达到这个限定值时,那么你就要小心考虑这种情况了,因为很明显即使没有达到这个限定值,cache仍然会进行删除操作。
2.如果一个key-entry已经被移除了,当你再次调用get(key)时,如果CacheBuilder采用的是CacheLoader模式,那依然会从cacheLoader中加载一次。
此外,如果你的cache里面的entries有着截然不同的内存占用如果你的cache values有着截然不同的内存占用,你可以通过CacheBuilder.weigher(Weigher)来为不同的entry设定weigh,然后使用CacheBuilder.maximumWeight(long)设定一个最大值。在tpn会通过local cache缓存用户对消息类目的订阅信息,有的用户订阅的消息类目比较多,所占的内存就比较多,有的用户订阅的消息类目比较少,自然占用的内存就比较少。那么我就可以通过下面的方法来根据用户订阅的消息类目数量设置不同的weight,这样就可以在不更改cache大小的情况下,使得缓存尽量覆盖更多地用户:
LoadingCache<Key, User> Users= CacheBuilder.newBuilder() .maximumWeight(100000) .weigher(new Weigher<Key, User>() { public int weigh(Key k, User u) { if(u.categories().szie() >5){ return 2; }else{ return 1; } } }) .build( new CacheLoader<Key, User>() { public Userload(Key key) { // no checked exception return createExpensiveUser(key); } });复制代码
PS:这个例子可能不是很恰当,当时足以说明weight的用法。
time-based eviction:基于时间的移除。Guava cache提供了两种方法来实现这个逻辑:
1. expireAfterAccess(long, TimeUnit)
从最后一次访问(读或者写)开始计时,过了这段指定的时间就会释放掉该entries。注意:那些被删掉的entries的顺序时和size-based eviction是十分相似的。
2. expireAfterWrite(long,TimeUnit)
从entries被创建或者最后一次被修改值的点来计时的,如果从这个点开始超过了那段指定的时间,entries就会被删除掉。这点设计的很精明,因为数据会随着时间变得越来越陈旧。
如果想要测试Timed Eviction,使用Ticker interface和CacheBuilder.ticker(Ticker)方法对你的cache设定一个时间即可,那么你就不需要去等待系统时间了。
reference-based eviction:基于引用的移除。Guava为你准备了entries的垃圾回收器,对于keys或者values可以使用weak reference ,对于values可以使用soft reference.
1. CacheBuilder.weakKeys(): 通过weak reference存储keys。在这种情况下,如果keys没有被strong或者soft引用,那么entries会被垃圾回收。
2. CacheBuilder.weakValues() : 通过weak referene 存储values.在这种情况下,如果valves没有被strong或者soft引用,那么entries会被垃圾回收。
需要注意的是:这种条件下的垃圾回收器是建立在引用之上的,那么这会造成整个cache是使用==来比较俩个key的,而不是equals();
除了上面这三种方式来移除cache的enties外,还可以通过以下3个方法来主动释放一些enties:
1. 单独移除用: Cache.invalidate(key)
2. 批量移除用 :Cache.invalidateAll(keys)
3. 移除所有用 :Cache.invalidateAll()
如果需要在移除数据的时候有所动作还可以定义Removal Listener,但是有点需要注意的是默认Removal Listener中的行为是和移除动作同步执行的,如果需要改成异步形式,可以考虑使用RemovalListeners.asynchronous(RemovalListener, Executor)。
最后我们来看一下Guava Cache是什么时候执行清理动作的。通过CacheBuilder创建的cache既不会自动执行清理和移除entry,也不会在entry过期后立马执行清除操作。相反,其在执行写操作或者读操作的时候(在写操作非常少的情况下)来通过少量的操作来执行清理工作。这样做的原因是:如果我们要不断进行缓存的清理和移除,我们需要创建一个线程,其业务将与用户的操作来争夺共享锁。此外,某些环境限制清理线程的创建,这将使CacheBuilder无法使用在该环境中。 因此,Guava cache将何时清理的选择权交给用户。如果你的缓存是面向高吞吐量应用的,那么你不必担心执行缓存维护,清理过期的entries等。如果你的缓存是面向读多写少的应用,为了避免影响缓存读取,那么就可以创建自己的维护线程每隔一段时间就调用Cache.cleanUp(),如可以用ScheduledExecutorService安排维修。
如果你想安排定期维护缓存的缓存中很少有写,只是用ScheduledExecutorService安排维修。
三、统计功能:
统计功能是Guava cache一个非常实用的特性。可以通过CacheBuilder.recordStats() 方法启动了 cache的数据收集:
1. Cache.stats(): 返回了一个CacheStats对象, 提供一些数据方法
2. hitRate(): 请求点击率
3. averageLoadPenalty(): 加载new value,花费的时间, 单位nanosecondes
4. evictionCount(): 清除的个数