guava-cache
Guava Cache适用场景:
- 你愿意消耗一部分内存来提升速度;
- 你已经预料某些值会被多次调用;
- 缓存数据不会超过内存总量;
Guava Cache是一个全内存的本地缓存实现,它提供了线程安全的实现机制。整体上来说Guava cache 是本地缓存的不二之选,简单易用,性能好。
创建方式
CacheLoader和Callable通过这两种方法创建的cache,和通常用map来缓存的做法比,不同在于这两种方法都实现了一种逻辑——从缓存中取key X的值,如果该值已经缓存过了,则返回缓存中的值,如果没有缓存过,可以通过某个方法来获取这个值。
但不同的在于cacheloader的定义比较宽泛, 是针对整个cache定义的,可以认为是统一的根据key值load value的方法。而callable的方式较为灵活,允许你在get的时候指定。
1、CacheLoader(guava)
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(jdk1.5)
public void testcallableCache()throws Exception{ Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).build(); String resultVal = cache.get("jerry", new Callable<String>() { 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); }
清除缓存的策略
任何Cache的容量都是有限的,而缓存清除策略就是决定数据在什么时候应该被清理掉。GuavaCache提了以下几种清除策略:
基于存活时间的清除(Timed Eviction)
这应该是最常用的清除策略,在构建Cache实例的时候,CacheBuilder提供两种基于存活时间的构建方法:
(1)expireAfterAccess(long, TimeUnit):缓存项在创建后,在给定时间内没有被读/写访问,则清除。
(2)expireAfterWrite(long, TimeUnit):缓存项在创建后,在给定时间内没有被写访问(创建或覆盖),则清除。
expireAfterWrite()方法有些类似于redis中的expire命令,但显然它只能设置所有缓存都具有相同的存活时间。若遇到一些缓存数据的存活时间为1分钟,一些为5分钟,那只能构建两个Cache实例了。
基于容量的清除(size-based eviction)
在构建Cache实例的时候,通过CacheBuilder.maximumSize(long)方法可以设置Cache的最大容量数,当缓存数量达到或接近该最大值时,Cache将清除掉那些最近最少使用的缓存。
以上是这种方式是以缓存的“数量”作为容量的计算方式,还有另外一种基于“权重”的计算方式。比如每一项缓存所占据的内存空间大小都不一样,可以看作它们有不同的“权重”(weights)。你可以使用CacheBuilder.weigher(Weigher)指定一个权重函数,并且用CacheBuilder.maximumWeight(long)指定最大总重。
显式清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收,Cache接口提供了如下API:
(1)个别清除:Cache.invalidate(key)
(2)批量清除:Cache.invalidateAll(keys)
(3)清除所有缓存项:Cache.invalidateAll()
基于引用的清除(Reference-based Eviction)
在构建Cache实例过程中,通过设置使用弱引用的键、或弱引用的值、或软引用的值,从而使JVM在GC时顺带实现缓存的清除,不过一般不轻易使用这个特性。
(1)CacheBuilder.weakKeys():使用弱引用存储键
(2)CacheBuilder.weakValues():使用弱引用存储值
(3)CacheBuilder.softValues():使用软引用存储值
清除什么时候发生?
也许这个问题有点奇怪,如果设置的存活时间为一分钟,难道不是一分钟后这个key就会立即清除掉吗?我们来分析一下如果要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工作,很多cache如redis、ehcache都是这样实现的。
但在GuavaCache中,并不存在任何线程!它实现机制是在写操作时顺带做少量的维护工作(如清除),偶尔在读操作时做(如果写操作实在太少的话),也就是说在使用的是调用线程,参考如下示例:
public class CacheService { static Cache<Integer, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(5, TimeUnit.SECONDS) .build(); public static void main(String[] args) throws Exception { new Thread() { //monitor public void run() { while(true) { SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); System.out.println(sdf.format(new Date()) +" size: "+cache.size()); try { Thread.sleep(2000); } catch (InterruptedException e) { } } }; }.start(); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss"); cache.put(1, "Hi"); System.out.println("write key:1 ,value:"+cache.getIfPresent(1)); Thread.sleep(10000); // when write ,key:1 clear cache.put(2, "bbb"); System.out.println("write key:2 ,value:"+cache.getIfPresent(2)); Thread.sleep(10000); // when read other key ,key:2 do not clear System.out.println(sdf.format(new Date()) +" after write, key:1 ,value:"+cache.getIfPresent(1)); Thread.sleep(2000); // when read same key ,key:2 clear System.out.println(sdf.format(new Date()) +" final, key:2 ,value:"+cache.getIfPresent(2)); } } 输出: 00:34:17 size: 0 write key:1 ,value:Hi 00:34:19 size: 1 00:34:21 size: 1 00:34:23 size: 1 00:34:25 size: 1 write key:2 ,value:bbb 00:34:27 size: 1 00:34:29 size: 1 00:34:31 size: 1 00:34:33 size: 1 00:34:35 size: 1 00:34:37 after write, key:1 ,value:null 00:34:37 size: 1 00:34:39 final, key:2 ,value:null 00:34:39 size: 0
通过分析发现:
(1)缓存项<1,"Hi">的存活时间是5秒,但经过5秒后并没有被清除,因为还是size=1
(2)发生写操作cache.put(2, "bbb")后,缓存项<1,"Hi">被清除,因为size=1,而不是size=2
(3)发生读操作cache.getIfPresent(1)后,缓存项<2,"bbb">没有被清除,因为还是size=1,看来读操作确实不一定会发生清除
(4)发生读操作cache.getIfPresent(2)后,缓存项<2,"bbb">被清除,因为读的key就是2
这在GuavaCache被称为“延迟删除”,即删除总是发生得比较“晚”,这也是GuavaCache不同于其他Cache的地方!这种实现方式的问题:缓存会可能会存活比较长的时间,一直占用着内存。如果使用了复杂的清除策略如基于容量的清除,还可能会占用着线程而导致响应时间变长。但优点也是显而易见的,没有启动线程,不管是实现,还是使用起来都让人觉得简单(轻量)。
如果你还是希望尽可能的降低延迟,可以创建自己的维护线程,以固定的时间间隔调用Cache.cleanUp(),ScheduledExecutorService可以帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的确定一定是自己的维护线程“命中”了维护的工作。
总结
请一定要记住GuavaCache的实现代码中没有启动任何线程!!Cache中的所有维护操作,包括清除缓存、写入缓存等,都是通过调用线程来操作的。这在需要低延迟服务场景中使用时尤其需要关注,可能会在某个调用的响应时间突然变大。
GuavaCache毕竟是一款面向本地缓存的,轻量级的Cache,适合缓存少量数据。如果你想缓存上千万数据,可以为每个key设置不同的存活时间,并且高性能,那并不适合使用GuavaCache。

浙公网安备 33010602011771号