毕竟西湖六月中, 风光不与四时同.
接天莲叶无穷碧, 映日荷花别样红.
晓出净慈寺送林子方 - 杨万里
周末与小伙伴约了一波西湖, 这个时间荷花开的正好..., 在开始文章之前先放一张 "佛系" 美图来镇楼!!!
最近这段时间用了下谷歌的 guava, 自己封了一个缓存模板方案, 特此记录, 以备后续所需.
一个缓存定时清除任务带来的 GC 问题
为什么要从这个来说起, 因为不说这个就没 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 是谷歌提供的一个基于内存的缓存工具包, 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 这个抽象类就可以了. 具体的代码贴在下面了.
完整的两个类
- 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;
- }
- }
- DefaultGuavaCacheManager
- 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 官方教程(中文版) https://willnewii.gitbooks.io/google-guava/content/
来源: https://juejin.im/post/5b2fb328f265da59a36e3f48