最近这段时间用了下谷歌的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;
}
}
}
Google Guava官方教程(中文版)