原文:https://juejin.cn/post/6844903624519188487
毕竟西湖六月中,风光不与四时同。
接天莲叶无穷碧,映日荷花别样红。
晓出净慈寺送林子方-杨万里

周末与小伙伴约了一波西湖,这个时间荷花开的正好…,在开始文章之前先放一张“佛系”美图来镇楼!!!
最近这段时间用了下谷歌的 guava,自己封了一个缓存模板方案,特此记录,以备后续所需。
一个缓存定时清除任务带来的GC问题
为什么要从这个来说起,因为不说这个就没 guava 什么事了!
最近项目中需要使用缓存来对一查查询频繁的数据做缓存处理;首先我们也不希望引入三方的如redis或者memcache这样的服务进来,其次是我们对于数据一致性的要求并不是很高,不需要集群内的查询接口共享到一份缓存数据;所以这样一来我们只要实现一个基于内存的缓存即可。
最开始我并没有考虑使用guava来做这个事情,而是自己写了一套基于CurrentHashMap的缓存方案;这里需要明确一点,因为缓存在这个场景里面希望提供超时清除的能力,而基于所以在自己缓存框架中增加了定时清除过期数据的能力。
这里我就直接把定时清楚的这段代码放上来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
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是谷歌提供的一个基于内存的缓存工具包,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 这个类了。
1 2 3
| @GwtCompatible(emulated = true) class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>
|
关于这个类的源码这里就不细说了,直接来看下在实际应用中我的封装思路【封装满足我当前的需求,如果有小伙伴需要借鉴,可以自己在做扩展】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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;
private Date resetTime;
private long highestSize = 0; private Date highestTime;
private volatile LoadingCache<K, V> cache;
|
这里先是定义了一些常量和基本的属性信息,当然这些属性会提供set&get方法,供实际使用时去自行设置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| public LoadingCache<K, V> getCache() { if(cache == null){ synchronized (this) { if(cache == null){ cache = CacheBuilder.newBuilder() .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中,我使用了一个静态内部类来创建当前默认的缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
private static class DefaultGuavaCache<String, Object> extends AbstractGuavaCache<String, Object> {
private static AbstractGuavaCache cache = new DefaultGuavaCache();
@Override protected Object fetchData(String key) { return null; }
public static AbstractGuavaCache getInstance() { return DefaultGuavaCache.cache; }
}
|
大概思路就是这样,如果需要扩展,我们只需要按照实际的需求去扩展AbstractGuavaCache这个抽象类就可以了。具体的代码贴在下面了。
完整的两个类
AbstractGuavaCache
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| 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; private static final int DEFAULT_SIZE = 100;
private int maxSize = MAX_SIZE;
private int expireTime = EXPIRE_TIME; private TimeUnit timeUnit = TimeUnit.MINUTES; private Date resetTime;
private long highestSize = 0; private Date highestTime;
private volatile LoadingCache<K, V> cache;
public LoadingCache<K, V> getCache() { if(cache == null){ synchronized (this) { if(cache == null){ cache = CacheBuilder.newBuilder() .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; }
protected abstract V fetchData(K key);
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; } }
|
DefaultGuavaCacheManager
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
| 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); }
public static void invalidate(String key) { cacheWrapper.getCache().invalidate(key); }
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; }
private static class DefaultGuavaCache<String, Object> extends AbstractGuavaCache<String, Object> {
private static AbstractGuavaCache cache = new DefaultGuavaCache();
@Override protected Object fetchData(String key) { return null; }
public static AbstractGuavaCache getInstance() { return DefaultGuavaCache.cache; }
}
}
|
参考
Google Guava官方教程(中文版)