毕竟西湖六月中,风光不与四时同。
接天莲叶无穷碧,映日荷花别样红。
晓出净慈寺送林子方-杨万里
周末与小伙伴约了一波西湖,这个时间荷花开的正好...,在开始文章之前先放一张“佛系”美图来镇楼!!!
最近这段时间用了下谷歌的guava,自己封了一个缓存模板方案,特此记录,以备后续所需。
为什么要从这个来说起,因为不说这个就没guava什么事了!
最近项目中需要使用缓存来对一查查询频繁的数据做缓存处理;首先我们也不希望引入三方的如redis或者memcache这样的服务进来,其次是我们对于数据一致性的要求并不是很高,不需要集群内的查询接口共享到一份缓存数据;所以这样一来我们只要实现一个基于内存的缓存即可。
最开始我并没有考虑使用guava来做这个事情,而是自己写了一套基于CurrentHashMap的缓存方案;这里需要明确一点,因为缓存在这个场景里面希望提供超时清除的能力,而基于所以在自己缓存框架中增加了定时清除过期数据的能力。
这里我就直接把定时清楚的这段代码放上来:
/** * 静态内部类来进行超时处理 */ private class ClearCacheThread extends Thread { @Override public void run() { while (true){ try { long now = System.currentTimeMillis(); Object[] keys = map.keySet().toArray(); for (Object key : keys) { CacheEntry entry = map.get(key); if (now - entry.time >= cacheTimeout) { synchronized (map) { map.remove(key); if (LOGGER.isDebugEnabled()){ LOGGER.debug("language cache timeout clear"); } } } } }catch (Exception e){ LOGGER.error("clear out time cache value error;",e); } } } }
这个线程是用来单独处理过期数据的。缓存初始化时就会触发这个线程的start方法开始执行。
正式由于这段代码的不合理导致我在发布dev环境之后,机器GC触发的频次高的离谱。在尝试了不同的修复方案之后,最后选择放弃了;改用guava了!
小伙伴们可以在下面留言来讨论下这里为什么会存在频繁GC的问题;我会把结论放在评论回复里面。
为什么选用guava呢,很显然,是大佬推荐的!!!
guava是谷歌提供的一个基于内存的缓存工具包,Guava Cache 提供了一种把数据(key-value对)缓存到本地(JVM)内存中的机制,适用于很少会改动的数据。Guava Cache 与 ConcurrentMap 很相似,但也不完全一样。最基本的区别是 ConcurrentMap 会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache 为了限制内存占用,通常都设定为自动回收元素。
对于我们的场景,guava 提供的能力满足了我们的需要:
既然选择它了,我们还是有必要来先对它有个大致的了解;先来看看它提供的一些类和接口:
接口/类 | 详细解释 |
---|---|
Cache | 【I】;定义get、put、invalidate等操作,这里只有缓存增删改的操作,没有数据加载的操作。 |
AbstractCache | 【C】;实现Cache接口。其中批量操作都是循环执行单次行为,而单次行为都没有具体定义。 |
LoadingCache | 【I】;继承自Cache。定义get、getUnchecked、getAll等操作,这些操作都会从数据源load数据。 |
AbstractLoadingCache | 【C】;继承自AbstractCache,实现LoadingCache接口。 |
LocalCache | 【C】;整个guava cache的核心类,包含了guava cache的数据结构以及基本的缓存的操作方法。 |
LocalManualCache | 【C】;LocalCache内部静态类,实现Cache接口。其内部的增删改缓存操作全部调用成员变量localCache(LocalCache类型)的相应方法。 |
LocalLoadingCache | 【C】;LocalCache内部静态类,继承自LocalManualCache类,实现LoadingCache接口。其所有操作也是调用成员变量localCache(LocalCache类型)的相应方法 |
CacheBuilder | 【C】;缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。CacheBuilder在build方法中,会把前面设置的参数,全部传递给LocalCache,它自己实际不参与任何计算 |
CacheLoader | 【C】;用于从数据源加载数据,定义load、reload、loadAll等操作。 |
整个来看的话,guava里面最核心的应该算是 LocalCache 这个类了。
@GwtCompatible(emulated = true) class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>
关于这个类的源码这里就不细说了,直接来看下在实际应用中我的封装思路【封装满足我当前的需求,如果有小伙伴需要借鉴,可以自己在做扩展】
private static final int MAX_SIZE = 1000; private static final int EXPIRE_TIME = 10; private static final int DEFAULT_SIZE = 100; private int maxSize = MAX_SIZE; private int expireTime = EXPIRE_TIME; /** 时间单位(分钟) */ private TimeUnit timeUnit = TimeUnit.MINUTES; /** Cache初始化或被重置的时间 */ private Date resetTime; /** 分别记录历史最多缓存个数及时间点*/ private long highestSize = 0; private Date highestTime; private volatile LoadingCache<K, V> cache;
这里先是定义了一些常量和基本的属性信息,当然这些属性会提供set&get方法,供实际使用时去自行设置。
public LoadingCache<K, V> getCache() { //使用双重校验锁保证只有一个cache实例 if(cache == null){ synchronized (this) { if(cache == null){ //CacheBuilder的构造函数是私有的,只能通过其静态方法newBuilder()来获得CacheBuilder的实例 cache = CacheBuilder.newBuilder() //设置缓存容器的初始容量为100 .initialCapacity(DEFAULT_SIZE) //缓存数据的最大条目 .maximumSize(maxSize) //定时回收:缓存项在给定时间内没有被写访问(创建或覆盖),则回收。 .expireAfterWrite(expireTime, timeUnit) //启用统计->统计缓存的命中率等 .recordStats() //设置缓存的移除通知 .removalListener((notification)-> { if (LOGGER.isDebugEnabled()){ LOGGER.debug("{} was removed, cause is {}" ,notification.getKey(), notification.getCause()); } }) .build(new CacheLoader<K, V>() { @Override public V load(K key) throws Exception { return fetchData(key); } }); this.resetTime = new Date(); this.highestTime = new Date(); if (LOGGER.isInfoEnabled()){ LOGGER.info("本地缓存{}初始化成功.", this.getClass().getSimpleName()); } } } } return cache; }
上面这段代码是整个缓存的核心,通过这段代码来生成我们的缓存对象【使用了单例模式】。具体的属性参数看注释。
因为上面的那些都是封装在一个抽象类AbstractGuavaCache里面的,所以我又封装了一个CacheManger用来管理缓存,并对外提供具体的功能接口;在CacheManger中,我使用了一个静态内部类来创建当前默认的缓存。
/** * 使用静态内部类实现一个默认的缓存,委托给manager来管理 * * DefaultGuavaCache 使用一个简单的单例模式 * @param <String> * @param <Object> */ private static class DefaultGuavaCache<String, Object> extends AbstractGuavaCache<String, Object> { private static AbstractGuavaCache cache = new DefaultGuavaCache(); /** * 处理自动载入缓存,按实际情况载入 * 这里 * @param key * @return */ @Override protected Object fetchData(String key) { return null; } public static AbstractGuavaCache getInstance() { return DefaultGuavaCache.cache; } }
大概思路就是这样,如果需要扩展,我们只需要按照实际的需求去扩展AbstractGuavaCache这个抽象类就可以了。具体的代码贴在下面了。
public abstract class AbstractGuavaCache<K, V> { protected final Logger LOGGER = LoggerFactory.getLogger(AbstractGuavaCache.class); private static final int MAX_SIZE = 1000; private static final int EXPIRE_TIME = 10; /** 用于初始化cache的参数及其缺省值 */ private static final int DEFAULT_SIZE = 100; private int maxSize = MAX_SIZE; private int expireTime = EXPIRE_TIME; /** 时间单位(分钟) */ private TimeUnit timeUnit = TimeUnit.MINUTES; /** Cache初始化或被重置的时间 */ private Date resetTime; /** 分别记录历史最多缓存个数及时间点*/ private long highestSize = 0; private Date highestTime; private volatile LoadingCache<K, V> cache; public LoadingCache<K, V> getCache() { //使用双重校验锁保证只有一个cache实例 if(cache == null){ synchronized (this) { if(cache == null){ //CacheBuilder的构造函数是私有的,只能通过其静态方法ne //wBuilder()来获得CacheBuilder的实例 cache = CacheBuilder.newBuilder() //设置缓存容器的初始容量为100 .initialCapacity(DEFAULT_SIZE) //缓存数据的最大条目 .maximumSize(maxSize) //定时回收:缓存项在给定时间内没有被写访问 //(创建或覆盖),则回收。 .expireAfterWrite(expireTime, timeUnit) //启用统计->统计缓存的命中率等 .recordStats() //设置缓存的移除通知 .removalListener((notification)-> { if (LOGGER.isDebugEnabled()){ //... } }) .build(new CacheLoader<K, V>() { @Override public V load(K key) throws Exception { return fetchData(key); } }); this.resetTime = new Date(); this.highestTime = new Date(); if (LOGGER.isInfoEnabled()){ //... } } } } return cache; } /** * 根据key从数据库或其他数据源中获取一个value,并被自动保存到缓存中。 * * 改方法是模板方法,子类需要实现 * * @param key * @return value,连同key一起被加载到缓存中的。 */ protected abstract V fetchData(K key); /** * 从缓存中获取数据(第一次自动调用fetchData从外部获取数据),并处理异常 * @param key * @return Value * @throws ExecutionException */ protected V getValue(K key) throws ExecutionException { V result = getCache().get(key); if (getCache().size() > highestSize) { highestSize = getCache().size(); highestTime = new Date(); } return result; } public int getMaxSize() { return maxSize; } public void setMaxSize(int maxSize) { this.maxSize = maxSize; } public int getExpireTime() { return expireTime; } public void setExpireTime(int expireTime) { this.expireTime = expireTime; } public TimeUnit getTimeUnit() { return timeUnit; } public void setTimeUnit(TimeUnit timeUnit) { this.timeUnit = timeUnit; } public Date getResetTime() { return resetTime; } public void setResetTime(Date resetTime) { this.resetTime = resetTime; } public long getHighestSize() { return highestSize; } public void setHighestSize(long highestSize) { this.highestSize = highestSize; } public Date getHighestTime() { return highestTime; } public void setHighestTime(Date highestTime) { this.highestTime = highestTime; } }
public class DefaultGuavaCacheManager { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultGuavaCacheManager.class); //缓存包装类 private static AbstractGuavaCache<String, Object> cacheWrapper; /** * 初始化缓存容器 */ public static boolean initGuavaCache() { try { cacheWrapper = DefaultGuavaCache.getInstance(); if (cacheWrapper != null) { return true; } } catch (Exception e) { LOGGER.error("Failed to init Guava cache;", e); } return false; } public static void put(String key, Object value) { cacheWrapper.getCache().put(key, value); } /** * 指定缓存时效 * @param key */ public static void invalidate(String key) { cacheWrapper.getCache().invalidate(key); } /** * 批量清除 * @param keys */ public static void invalidateAll(Iterable<?> keys) { cacheWrapper.getCache().invalidateAll(keys); } /** * 清除所有缓存项 : 慎用 */ public static void invalidateAll() { cacheWrapper.getCache().invalidateAll(); } public static Object get(String key) { try { return cacheWrapper.getCache().get(key); } catch (Exception e) { LOGGER.error("Failed to get value from guava cache;", e); } return null; } /** * 使用静态内部类实现一个默认的缓存,委托给manager来管理 * * DefaultGuavaCache 使用一个简单的单例模式 * @param <String> * @param <Object> */ private static class DefaultGuavaCache<String, Object> extends AbstractGuavaCache<String, Object> { private static AbstractGuavaCache cache = new DefaultGuavaCache(); /** * 处理自动载入缓存,按实际情况载入 * @param key * @return */ @Override protected Object fetchData(String key) { return null; } public static AbstractGuavaCache getInstance() { return DefaultGuavaCache.cache; } } }