在工作中,加Cache是非常常见的一种性能优化手段,操作系统底层、计算机硬件层为了性能优化加了各种各样的Cache,当然大多数都是对应用层透明的。但如果你想在应用层加Cache的话,可能就需要你自己实现了。
其实在Java环境下,Cache有各种各样的选择,比如最初级的你可以直接用HashMap实现一个Cache,不过你得自己关注下数据加载和淘汰的策略。更高级的有像spring-cache,代码都不需要改,只需要简单加几个注解就可以实现对关键数据的缓存,相当方便(后续我也会出一篇博客介绍下spring-cache)。 今天我们要介绍的是谷歌guava包中的LoadingCache, 也是功能完善,简单好用。
LoadingCache是Guava包中提供一个一种本地Cache,本地Cache的优势就是没有网络IO,速度快。但劣势也很明显,Cache容量受限于本地内存大小,Cache中的数据没法共享。所以它就只适合少量热点数据的缓存,其使用方法也很简单,我们拿maven为例,你只需要添加一下Maven依赖即可引入guava包:
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>31.1-jreversion>
dependency>
使用代码也非常简单,如下:
private static LoadingCache<String, String> cache =
CacheBuilder.newBuilder()
// 初始化容量
.initialCapacity(4)
// 缓存池大小,在缓存数量到达该大小时, Guava开始回收旧的数据
.maximumSize(8)
// 设置时间对象没有被读/写访问则对象从内存中删除(在另外的线程里面不定期维护)
.expireAfterAccess(5, TimeUnit.SECONDS)
// 设置缓存在写入之后 设定时间 后失效
.expireAfterWrite(5, TimeUnit.SECONDS)
// 数据被移除时的监听器, 缓存项被移除时会触发执行
.removalListener((RemovalListener<String, String>) rn -> {
System.out.println(String.format("数据key:%s value:%s 因为%s被移除了", rn.getKey(), rn.getValue(),
rn.getCause().name()));
})
// 开启Guava Cache的统计功能
.recordStats()
// 数据写入后被多久刷新一次
.refreshAfterWrite(5, TimeUnit.SECONDS)
// 数据并发级别
.concurrencyLevel(16)
// 当缓存中没有数据时的数据加载器
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
return key + "_" + System.currentTimeMillis();
}
});
然后我们就可以直接在代码的其他地方用cache.get("myKey")
来愉快地使用LoadingCache了,它会主动加载数据,并在存储空间不够或者数据过期时清理掉不需要的数据,非常省心且方便。
这里有些重点参数,下面详细介绍下:
参数 | 作用 | 注意事项 |
---|---|---|
maximumSize | 缓存的k-v最大数据,当总缓存的数据量达到或者快达到这个值时,就会淘汰它认为不太用的一份数据,近似LRU或者LFU策略 | 并不一定是达到这个值才开始淘汰旧数据,可能接近时就会开始淘汰 |
expireAfterAccess | 数据被访问后多久就会过期,这个策略主要是为了淘汰长时间不被访问的数据 | 数据过期不是立即淘汰,而是有数据访问时才会触发 |
expireAfterWrite | 数据写入后多久过期,这个策略是为了防止旧数据被缓存过久 | 同上 |
refreshAfterWrite | 数据写入后多久刷新一次,这个类似于expireAfterWrite,但它会主动更新数据 | 同上 |
concurrencyLevel | 数据的并发级别,LoadingCache为了实现线程安全,它里面采用了类似Java7中ConcurrentHashMap的实现,采用了分段加锁的方式,分段数影响了它的最大并发量 | |
recordStats | 开启Cache的状态统计(默认是开启的) | 开启这个是会影响到性能的,如果要求极致性能的话关注下个 |
我们来重点介绍下CacheLoader CacheStats和RemovalListener,因为这三者涉及到了数据的加载、使用和删除的完整生命周期,先来看下CacheLoader。
CacheLoader的作用就是为了在Cache中数据缺失时加载数据,其中最重要的方法就是load()方法,你可以在load() 方法中实现对应key加载数据的逻辑。在调用LoadingCache的get(key)方法时,如果key对应的value不存在,LoadingCache就会调起你在创建cache时传入的CacheLoader的load方法。
使用CacheStats cacheStats = cache.stats();
我就可以获取到cache的stats数据。从cacheStats中我们可以看到cache的命中率、命中数、异常率、加载时延……等数据,通过这些数据就可以直观地看出我们cache的一些性能指标,如果做出一些参数调整。 比如如果命中率过低,我们是不是可以调整大下maximumSize,或者调整下数据的过期策略?
RemovalListener会在LoadingCache中数据被清理时调起,其实就是个监听器模式,这样你可以通过Listener实现对数据淘汰事件的监听,比如在数据淘汰时打一行日志啥的。使用方法也很简单,在Java8+上你可以直接使用lambda表达式,或者也可以自己实现RemovalListener接口,并在构建Cache时注册进去即可。
public enum RemovalCause {
EXPLICIT {
@Override
boolean wasEvicted() {
return false;
}
},
REPLACED {
@Override
boolean wasEvicted() {
return false;
}
},
COLLECTED {
@Override
boolean wasEvicted() {
return true;
}
},
EXPIRED {
@Override
boolean wasEvicted() {
return true;
}
},
SIZE {
@Override
boolean wasEvicted() {
return true;
}
};
abstract boolean wasEvicted();
}
在RemovalListener内,我们可以通过RemovalListener获取到被删除的数据的key和value,也可以知晓数据被删除的原因。可以看到有个RemovalCause枚举类,详细说明了几种数据被清除的原因,比如被用户主动删除(RemovalCause.EXPLICIT),被替换(RemovalCause.REPLACED),过期淘汰(RemovalCause.EXPIRED),被GC收集器删除(RemovalCause.COLLECTED),容量不够导致的删除(RemovalCause.SIZE)。
关于LoadingCache的介绍就到这了。再说下谷歌的guava包,其实guava是一个很好用的Java开源开发包,里面除了cache之外,还有各种集合工具、并发工具,Cache只是其中很小的一部分,后续有机会我们在详细探索下guava。今天的文章就到这了,大家觉得有用请点赞,喜欢请关注。