https://www.cnblogs.com/boothsun/p/5848143.htmlhtml
版权声明:本文为博主原创文章,未经博主容许不得转载。 http://blog.csdn.net/u012859681/article/details/75220605redis
缓存在应用中是必不可少的,常常用的如redis、memcache以及内存缓存等。Guava是Google出的一个工具包,它里面的cache便是对本地内存缓存的一种实现,支持多种缓存过时策略。
Guava cache的缓存加载方式有两种:缓存
具体两种方式的介绍看官方文档:http://ifeve.com/google-guava-cachesexplained/并发
接下来看看常见的一些使用方法。
后面的示例实践都是以CacheLoader方式加载缓存值。异步
LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
如代码所示新建了名为caches的一个缓存对象,maximumSize定义了缓存的容量大小,当缓存数量即将到达容量上线时,则会进行缓存回收,回收最近没有使用或整体上不多使用的缓存项。须要注意的是在接近这个容量上限时就会发生,因此在定义这个值的时候须要视状况适量地增大一点。
另外经过expireAfterWrite这个方法定义了缓存的过时时间,写入十分钟以后过时。
在build方法里,传入了一个CacheLoader对象,重写了其中的load方法。当获取的缓存值不存在或已过时时,则会调用此load方法,进行缓存值的计算。
这就是最简单也是咱们日常最经常使用的一种使用方法。定义了缓存大小、过时时间及缓存值生成方法。ide
若是用其余的缓存方式,如redis,咱们知道上面这种“若是有缓存则返回;不然运算、缓存、而后返回”的缓存模式是有很大弊端的。当高并发条件下同时进行get操做,而此时缓存值已过时时,会致使大量线程都调用生成缓存值的方法,好比从数据库读取。这时候就容易形成数据库雪崩。这也就是咱们常说的“缓存穿透”。
而Guava cache则对此种状况有必定控制。当大量线程用相同的key获取缓存值时,只会有一个线程进入load方法,而其余线程则等待,直到缓存值被生成。这样也就避免了缓存穿透的危险。高并发
如上的使用方法,虽然不会有缓存穿透的状况,可是每当某个缓存值过时时,总是会致使大量的请求线程被阻塞。而Guava则提供了另外一种缓存策略,缓存值定时刷新:更新线程调用load方法更新该缓存,其余请求线程返回该缓存的旧值。这样对于某个key的缓存来讲,只会有一个线程被阻塞,用来生成缓存值,而其余的线程都返回旧的缓存值,不会被阻塞。
这里就须要用到Guava cache的refreshAfterWrite方法。以下所示:工具
LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .refreshAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
如代码所示,每隔十分钟缓存值则会被刷新。fetch
此外须要注意一个点,这里的定时并非真正意义上的定时。Guava cache的刷新须要依靠用户请求线程,让该线程去进行load方法的调用,因此若是一直没有用户尝试获取该缓存值,则该缓存也并不会刷新。
如2中的使用方法,解决了同一个key的缓存过时时会让多个线程阻塞的问题,只会让用来执行刷新缓存操做的一个用户线程会被阻塞。由此能够想到另外一个问题,当缓存的key不少时,高并发条件下大量线程同时获取不一样key对应的缓存,此时依然会形成大量线程阻塞,而且给数据库带来很大压力。这个问题的解决办法就是将刷新缓存值的任务交给后台线程,全部的用户请求线程均返回旧的缓存值,这样就不会有用户线程被阻塞了。
详细作法以下:
ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20)); LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .refreshAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } @Override public ListenableFuture<Object> reload(String key, Object oldValue) throws Exception { return backgroundRefreshPools.submit(new Callable<Object>() { @Override public Object call() throws Exception { return generateValueByKey(key); } }); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
在上面的代码中,咱们新建了一个线程池,用来执行缓存刷新任务。而且重写了CacheLoader的reload方法,在该方法中创建缓存刷新的任务并提交到线程池。
注意此时缓存的刷新依然须要靠用户线程来驱动,只不过和2不一样之处在于该用户线程触发刷新操做以后,会立马返回旧的缓存值。
能够看到防缓存穿透和防用户线程阻塞都是依靠返回旧值来完成的。因此若是没有旧值,一样会所有阻塞,所以应视状况尽可能在系统启动时将缓存内容加载到内存中。
在刷新缓存时,若是generateValueByKey方法出现异常或者返回了null,此时旧值不会更新。
题外话:在使用内存缓存时,切记拿到缓存值以后不要在业务代码中对缓存直接作修改,由于此时拿到的对象引用是指向缓存真正的内容的。若是须要直接在该对象上进行修改,则在获取到缓存值后拷贝一份副本,而后传递该副本,进行修改操做。(我曾经就犯过这个低级错误 - -!)
以下为基于Guava cache抽象出来的一个缓存工具类。(抽象得很差,勉强能用 - -!)。
有改进意见麻烦多多指教。
/** * @description: 利用guava实现的内存缓存。缓存加载以后永不过时,后台线程定时刷新缓存值。刷新失败时将继续返回旧缓存。 * 须要在子类中初始化refreshDuration、refreshTimeunitType、cacheMaximumSize三个参数 * 后台刷新线程池为该系统中全部子类共享,大小为20. * @author: luozhuo * @date: 2017年6月21日 上午10:03:45 * @version: V1.0.0 * @param <K> * @param <V> */ public abstract class ZorroGuavaCache <K, V> { /** * 缓存自动刷新周期 */ protected int refreshDuration; /** * 缓存刷新周期时间格式 */ protected TimeUnit refreshTimeunitType; /** * 缓存最大容量 */ protected int cacheMaximumSize; private LoadingCache<K, V> cache; private ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20)); /** * @description: 初始化全部protected字段: * refreshDuration、refreshTimeunitType、cacheMaximumSize * @author: luozhuo * @date: 2017年6月13日 下午2:49:19 */ protected abstract void initCacheFields(); /** * @description: 定义缓存值的计算方法 * @description: 新值计算失败时抛出异常,get操做时将继续返回旧的缓存 * @param key * @return * @author: luozhuo * @throws Exception * @date: 2017年6月14日 下午7:11:10 */ protected abstract V getValueWhenExpire(K key) throws Exception; /** * @description: 提供给外部使用的获取缓存方法,由实现类进行异常处理 * @param key * @return * @author: luozhuo * @date: 2017年6月15日 下午12:00:57 */ public abstract V getValue(K key); /** * @description: 获取cache实例 * @return * @author: luozhuo * @date: 2017年6月13日 下午2:50:11 */ private LoadingCache<K, V> getCache() { if(cache == null){ synchronized (this) { if(cache == null){ initCacheFields(); cache = CacheBuilder.newBuilder() .maximumSize(cacheMaximumSize) .refreshAfterWrite(refreshDuration, refreshTimeunitType) .build(new CacheLoader<K, V>() { @Override public V load(K key) throws Exception { return getValueWhenExpire(key); } @Override public ListenableFuture<V> reload(final K key, V oldValue) throws Exception { return backgroundRefreshPools.submit(new Callable<V>() { public V call() throws Exception { return getValueWhenExpire(key); } }); } }); } } } return cache; } /** * @description: 从cache中拿出数据的操做 * @param key * @return * @throws ExecutionException * @author: luozhuo * @date: 2017年6月13日 下午5:07:11 */ protected V fetchDataFromCache(K key) throws ExecutionException { return getCache().get(key); } }