转载

Google Guava 在实际场景中的应用封装

毕竟西湖六月中,风光不与四时同。

接天莲叶无穷碧,映日荷花别样红。

晓出净慈寺送林子方-杨万里

Google Guava 在实际场景中的应用封装

周末与小伙伴约了一波西湖,这个时间荷花开的正好...,在开始文章之前先放一张“佛系”美图来镇楼!!!

最近这段时间用了下谷歌的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的问题;我会把结论放在评论回复里面。

Google Guava 在实际场景中的应用封装

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;
        }

    }

}
原文  https://juejin.im/post/5b2fb328f265da59a36e3f48
正文到此结束
Loading...