对Guava Cache的封装和使用

时间:2021-07-22 15:51:29

由于项目的实际情况,需要缓存一些比较不经常改动的数据在本地服务器中,以提高接口处理的速度。决定采用Guava Cache之后,整理了一些具体需求:

  1. 由于要缓存的key-value对比较多,需要一个封装好的类被继承,子类可以简单的实现把key-value缓存到Guava Cache中;
  2. 需要定义一个接口,简单的定义一个get(K key)方法,方便使用;
  3. 需要有一个管理界面,统计缓存的命中率、记录数,以便以后做出相应的调整;
  4. 需要有一个管理界面,重设、清空缓存中的数据,或使缓存中的数据失效,以强行让服务器重新从数据库获取数据,并记录重置的时间;
  5. 需要有一个管理界面,分页查看缓存中的具体内容。

现在,该系统已经实现,并已经在正式环境中运行了一段时间,日均总命中次数超过一百万,大部分缓存的命中率在98%以上,为某些接口的请求节省了一半的时间。

Guava Cache简介:

Guava Cache提供了一种把数据(key-value对)缓存到本地(JVM)内存中的机制,适用于很少会改动的数据,比如地区信息、系统配置、字典数据,等。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。

本文介绍了一种对Guava LoadingCache的封装使用,并提供管理页面的实现。

首先,介绍一些Guava Cache的基本概念:

Guava提供两种不同的方法来加载数据:
  • CacheLoader:在build cache的时候定义一个CacheLoader来获取数据,适用的情况:有固定的方式可以根据key来加载或计算value的值,比如从数据库中获取数据
  • Callable:在get的时候传入一个Callable对象,适用的情况:如果从缓存中获取不到数据,则另外计算一个出来,并把计算结果加入到缓存中
另外,还可以使用cache.put(key, value)方法直接向缓存中插入值,但不推荐使用,因为这样会多了一步操作。

缓存回收方式:
1、基于容量的回收(size-based eviction),有两种方式,接近最大的size或weight时回收:
  • 基于maximumSize(long):一个数据项占用一个size单位,适用于value是固定大小的情况
  • 基于maximumWeight(long):对不同的数据项计算weight,适用于value不定大小的情况,比如value为Map类型时,可以把map.size()作为weight
2、定时回收(Timed Eviction)
  • expireAfterAccess(long, TimeUnit):缓存项在给定时间内没有被读/写,则回收。
  • expireAfterWrite(long, TimeUnit):缓存项在给定时间内没有被写访问(创建或覆盖),则回收。
3、基于引用的回收(Reference-based Eviction),通过使用弱引用的键或值、或软引用的值,把缓存设置为允许垃圾回收器回收:
  • CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被GC回收
  • CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被GC回收
  • CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。影响性能,不推荐使用。
4、显式清除(invalidate)
  • 个别清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有缓存项:Cache.invalidateAll()

什么时候发生缓存清理:
使用CacheBuilder构建的缓存不会"自动"执行清理和回收工作,也不会在某个缓存项过期后马上清理,也没有诸如此类的清理机制。它是在写操作时顺带做少量的维护工作(清理);如果写操作太少,读操作的时候也会进行少量维护工作。

基于容量的回收原则:
基本原则是LRU,但是按照每个Segment来清除的。比如:
一个maximumSize为100的Cache,concurrencyLevel=4,则如果开始清除缓存时,那些segment中size>25的会被优先清除掉只剩下25个。

移除监听器(Removal Listener):
通过 CacheBuilder.removalListener(RemovalListener),可以声明一个监听器,以便缓存项被移除时做一些额外操作,RemovalListener会获取移除通知[RemovalNotification],里面包含移除原因[RemovalCause]、键和值。

注:耗性能。可以使用 RemovalListeners.asynchronous(RemovalListener, Executor)定义监听器为异步操作。

统计功能:
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供一些统计信息。具体信息可以查看CacheStats类的定义。

asMap视图:
asMap视图提供了缓存的ConcurrentMap形式,但它的get方法不会往缓存中加入数据,实质上等同于 cache.getIfPresent(key)。可以用它来遍历查看缓存中的所有数据。


下面介绍对Guava Cache进行封装使用的具体方法:

使用的设计模式:策略模式(Strategy)
关于这种设计模式,具体请参考: http://zz563143188.iteye.com/blog/1847029 (第13种)

这种设计模式的关系图如下:

对Guava Cache的封装和使用

总策略:利用Guava Cache来存放我们自己的数据。
图中3+1种角色和代码的对应关系如下:
  1. Context:Service.java,(策略的使用者)
  2. Strategy:ILocalCache.java,(定义策略的接口)
  3. ConcreteStrategy:LCAreaIdToArea.java,其他ILocalCache实现类 ……,(实现策略的类)
  4. 辅助类:GuavaAbstractLoadingCache.java,(实现策略的辅助类,就是封装了Guava Cache的一个类)

各角色对应的具体功能如下:
  1. Service:cache的使用者
  2. Cache接口:定义一个get()方法,通过key获取value
  3. Cache实现类:利用辅助类,实现Cache接口的get()方法
  4. Guava Cache实现辅助类:封装了对Guava Cache的利用,包括cache的创建、从数据源获取数据、定义过时策略、等
除了上面的核心功能模块,其他辅助的功能模块如下:
Cache管理类:封装了对所有实现类进行管理的一些方法,包括清空Cache中的数据、查看数据、查看统计信息、等
Cache Controller:调用Cache管理类,为管理页面提供Web HTTP接口
Cache管理页面:Web页面,用于查看Cache列表、统计信息、数据,清空缓存,等

各模块的具体代码,代码中已经包括了比较详尽的注解:

主要的依赖包:

<dependency>  
    <groupId>com.google.guava</groupId>  
    <artifactId>guava</artifactId>  
    <version>18.0</version>  
</dependency> 
Cache接口:定义一个get()方法,通过key获取value

/** 
 * 本地缓存接口 
 * @author XuJijun 
 * 
 * @param <K> Key的类型 
 * @param <V> Value的类型 
 */  
public interface ILocalCache <K, V> {  
      
    /** 
     * 从缓存中获取数据 
     * @param key 
     * @return value 
     */  
    public V get(K key);  
}  
Guava Cache实现辅助类:封装了对Guava Cache的利用,包括cache的创建、从数据源获取数据、定义过时策略、等

package com.xjj.cache.guava;  
  
/** 
 * 抽象Guava缓存类、缓存模板。 
 * 子类需要实现fetchData(key),从数据库或其他数据源(如Redis)中获取数据。 
 * 子类调用getValue(key)方法,从缓存中获取数据,并处理不同的异常,比如value为null时的InvalidCacheLoadException异常。 
 *  
 * @author XuJijun 
 * @Date 2015-05-18 
 * 
 * @param <K> key 类型 
 * @param <V> value 类型 
 */  
public abstract class GuavaAbstractLoadingCache <K, V> {  
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());  
      
    //用于初始化cache的参数及其缺省值  
    private int maximumSize = 1000;                 //最大缓存条数,子类在构造方法中调用setMaximumSize(int size)来更改  
    private int expireAfterWriteDuration = 60;      //数据存在时长,子类在构造方法中调用setExpireAfterWriteDuration(int duration)来更改  
    private TimeUnit timeUnit = TimeUnit.MINUTES;   //时间单位(分钟)  
      
    private Date resetTime;     //Cache初始化或被重置的时间  
    private long highestSize=0; //历史最高记录数  
    private Date highestTime;   //创造历史记录的时间  
      
    private LoadingCache<K, V> cache;  
      
    /** 
     * 通过调用getCache().get(key)来获取数据  
     * @return cache 
     */  
    public LoadingCache<K, V> getCache() {  
        if(cache == null){  //使用双重校验锁保证只有一个cache实例  
            synchronized (this) {  
                if(cache == null){  
                    cache = CacheBuilder.newBuilder().maximumSize(maximumSize)      //缓存数据的最大条目,也可以使用.maximumWeight(weight)代替  
                            .expireAfterWrite(expireAfterWriteDuration, timeUnit)   //数据被创建多久后被移除  
                            .recordStats()                                          //启用统计  
                            .build(new CacheLoader<K, V>() {  
                                @Override  
                                public V load(K key) throws Exception {  
                                    return fetchData(key);  
                                }  
                            });  
                    this.resetTime = new Date();  
                    this.highestTime = new Date();  
                    logger.debug("本地缓存{}初始化成功", this.getClass().getSimpleName());  
                }  
            }  
        }  
          
        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 long getHighestSize() {  
        return highestSize;  
    }  
      
    public Date getHighestTime() {  
        return highestTime;  
    }  
      
    public Date getResetTime() {  
        return resetTime;  
    }  
  
    public void setResetTime(Date resetTime) {  
        this.resetTime = resetTime;  
    }  
  
    public int getMaximumSize() {  
        return maximumSize;  
    }  
  
    public int getExpireAfterWriteDuration() {  
        return expireAfterWriteDuration;  
    }  
  
    /** 
     * 设置最大缓存条数 
     * @param maximumSize 
     */  
    public void setMaximumSize(int maximumSize) {  
        this.maximumSize = maximumSize;  
    }  
  
    /** 
     * 设置数据存在时长(分钟) 
     * @param expireAfterWriteDuration 
     */  
    public void setExpireAfterWriteDuration(int expireAfterWriteDuration) {  
        this.expireAfterWriteDuration = expireAfterWriteDuration;  
    }  
}  
Cache实现类:利用辅助类,实现Cache接口的get()方法,(以一个areaId -> Area为例子)

package com.xjj.entity;  
  
public class Area {  
    private int id;  
    private int parentCode;  
    private String name;  
    private int code;  
    private String pinyin;  
    private int type;  
  
    public char getFirstLetter(){  
        return pinyin.charAt(0);  
    }  
  
//省略其他getter和setter  
}  
  
package com.xjj.cache.local.impl;  
  
/** 
 * 本地缓存:areaId -> Area 
 * @author XuJijun 
 * 
 */  
@Component  
public class LCAreaIdToArea extends GuavaAbstractLoadingCache<Integer, Area> implements ILocalCache<Integer, Area> {  
    //@Autowired  
    //private AreasDAO areasDAO;  
      
    //由Spring来维持单例模式  
      
    private LCAreaIdToArea(){  
        setMaximumSize(3000); //最大缓存条数  
    }  
      
    @Override  
    public Area get(Integer key) {  
        try {  
            return getValue(key);  
        } catch (Exception e) {  
            logger.error("无法根据areaId={}获取Area,可能是数据库中无该记录。", key ,e);  
            return null;  
        }  
    }  
  
    /** 
     * 从数据库中获取数据 
     */  
    @Override  
    protected Area fetchData(Integer key) {  
        logger.debug("测试:正在从数据库中获取area,area id={}", key);  
        //return areasDAO.getAreaById(key);  
        //测试专用,实际项目使用areaDao从数据库中获取数据  
        Area a = new Area();  
        a.setCode(key);  
        a.setId(key);  
        a.setName("地区:"+key);  
        a.setParentCode(Integer.valueOf(key.toString().substring(0, key.toString().length()-3)));  
        a.setPinyin("pinyin:"+key);  
        a.setType(AreaType.CITY.getValue());  
          
        return a;  
    }  
}  
Service:cache的使用者

/** 
 * Area相关方法,使用缓存 
 * @author XuJijun 
 * 
 */  
@Service  
public class AreaService implements IAreaService {  
    @Resource(name="LCAreaIdToArea")  
    ILocalCache<Integer, Area> lCAreaIdToArea;  
  
    /** 
     * 根据areaId获取Area 
     * @param areaId 
     * @return Area 
     */   
    @Override  
    public Area getAreaById(int areaId) {  
        return lCAreaIdToArea.get(areaId);  
    }  
  
}  

Cache管理类:封装了对所有实现类进行管理的一些方法,包括清空Cache中的数据、查看数据、查看统计信息、等
代码中所涉及到的其他类(SpringContextUtil、PageParams、PageResult)请参考源代码: https://github.com/xujijun/MyJavaStudio
package com.xjj.cache.guava;  
  
/** 
 * Guava缓存监视和管理工具 
 * @author XuJijun 
 * 
 */  
public class GuavaCacheManager {  
    //保存一个Map: cacheName -> cache Object,以便根据cacheName获取Guava cache对象  
    private static Map<String, ? extends GuavaAbstractLoadingCache<Object, Object>> cacheNameToObjectMap = null;  
  
    /** 
     * 获取所有GuavaAbstractLoadingCache子类的实例,即所有的Guava Cache对象 
     * @return 
     */  
      
    @SuppressWarnings("unchecked")  
    private static Map<String, ? extends GuavaAbstractLoadingCache<Object, Object>> getCacheMap(){  
        if(cacheNameToObjectMap==null){  
            cacheNameToObjectMap = (Map<String, ? extends GuavaAbstractLoadingCache<Object, Object>>) SpringContextUtil.getBeanOfType(GuavaAbstractLoadingCache.class);  
        }  
        return cacheNameToObjectMap;  
          
    }  
      
    /** 
     *  根据cacheName获取cache对象  
     * @param cacheName 
     * @return 
     */  
    private static GuavaAbstractLoadingCache<Object, Object> getCacheByName(String cacheName){  
        return (GuavaAbstractLoadingCache<Object, Object>) getCacheMap().get(cacheName);  
    }  
      
    /** 
     * 获取所有缓存的名字(即缓存实现类的名称) 
     * @return 
     */  
    public static Set<String> getCacheNames() {  
        return getCacheMap().keySet();  
    }  
      
    /** 
     * 返回所有缓存的统计数据 
     * @return List<Map<统计指标,统计数据>> 
     */  
    public static ArrayList<Map<String, Object>> getAllCacheStats() {  
          
        Map<String, ? extends Object> cacheMap = getCacheMap();  
        List<String> cacheNameList = new ArrayList<>(cacheMap.keySet());  
        Collections.sort(cacheNameList);//按照字母排序  
  
        //遍历所有缓存,获取统计数据  
        ArrayList<Map<String, Object>> list = new ArrayList<>();  
        for(String cacheName : cacheNameList){  
            list.add(getCacheStatsToMap(cacheName));  
        }  
          
        return list;  
    }  
      
    /** 
     * 返回一个缓存的统计数据 
     * @param cacheName 
     * @return Map<统计指标,统计数据> 
     */  
    private static Map<String, Object> getCacheStatsToMap(String cacheName) {  
        Map<String, Object> map =  new LinkedHashMap<>();  
        GuavaAbstractLoadingCache<Object, Object> cache = getCacheByName(cacheName);  
        CacheStats cs = cache.getCache().stats();  
        NumberFormat percent = NumberFormat.getPercentInstance(); // 建立百分比格式化用  
        percent.setMaximumFractionDigits(1); // 百分比小数点后的位数  
        map.put("cacheName", cacheName);  
        map.put("size", cache.getCache().size());  
        map.put("maximumSize", cache.getMaximumSize());  
        map.put("survivalDuration", cache.getExpireAfterWriteDuration());  
        map.put("hitCount", cs.hitCount());  
        map.put("hitRate", percent.format(cs.hitRate()));  
        map.put("missRate", percent.format(cs.missRate()));  
        map.put("loadSuccessCount", cs.loadSuccessCount());  
        map.put("loadExceptionCount", cs.loadExceptionCount());  
        map.put("totalLoadTime", cs.totalLoadTime()/1000000);       //ms  
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");  
        if(cache.getResetTime()!=null){  
            map.put("resetTime", df.format(cache.getResetTime()));  
        }  
        map.put("highestSize", cache.getHighestSize());  
        if(cache.getHighestTime()!=null){  
            map.put("highestTime", df.format(cache.getHighestTime()));    
        }  
          
        return map;  
    }  
      
    /** 
     * 根据cacheName清空缓存数据 
     * @param cacheName 
     */  
    public static void resetCache(String cacheName){  
        GuavaAbstractLoadingCache<Object, Object> cache = getCacheByName(cacheName);  
        cache.getCache().invalidateAll();  
        cache.setResetTime(new Date());  
    }  
  
    /** 
     * 分页获得缓存中的数据  
     * @param pageParams 
     * @return 
     */  
    public static PageResult<Object> queryDataByPage(PageParams<Object> pageParams) {  
        PageResult<Object> data = new PageResult<>(pageParams);  
          
        GuavaAbstractLoadingCache<Object, Object> cache = getCacheByName((String) pageParams.getParams().get("cacheName"));  
        ConcurrentMap<Object, Object> cacheMap = cache.getCache().asMap();  
        data.setTotalRecord(cacheMap.size());  
        data.setTotalPage((cacheMap.size()-1)/pageParams.getPageSize()+1);  
          
        //遍历  
        Iterator<Entry<Object, Object>> entries = cacheMap.entrySet().iterator();  
        int startPos = pageParams.getStartPos()-1;  
        int endPos = pageParams.getEndPos()-1;   
        int i=0;  
        Map<Object, Object> resultMap = new LinkedHashMap<>();  
        while (entries.hasNext()) {  
            Map.Entry<Object, Object> entry = entries.next();  
            if(i>endPos){  
                break;  
            }  
              
            if(i>=startPos){  
                resultMap.put(entry.getKey(), entry.getValue());  
            }  
              
            i++;  
        }  
        List<Object> resultList = new ArrayList<>();  
        resultList.add(resultMap);  
        data.setResults(resultList);  
        return data;  
    }  
}  

Cache Controller:调用Cache管理类,为管理页面提供Web HTTP接口

package com.xjj.web.controller;  
  
/** 
 * 本地缓存管理接口:统计信息查询、重置数据……等 
 * @author XuJijun 
 * 
 */  
@RestController  
@RequestMapping("/cache/admin")  
public class CacheAdminController {  
      
    /** 
     * 查询cache统计信息 
     * @param cacheName 
     * @return cache统计信息 
     */  
    @RequestMapping(value = "/stats", method = RequestMethod.POST)  
    public JsonResult cacheStats(String cacheName) {  
        JsonResult jsonResult = new JsonResult();  
          
        //暂时只支持获取全部  
          
        switch (cacheName) {  
        case "*":  
            jsonResult.setData(GuavaCacheManager.getAllCacheStats());  
            jsonResult.setMessage("成功获取了所有的cache!");  
            break;  
  
        default:  
            break;  
        }  
          
        return jsonResult;  
    }  
      
    /** 
     * 清空缓存数据、并返回清空后的统计信息 
     * @param cacheName 
     * @return 
     */  
    @RequestMapping(value = "/reset", method = RequestMethod.POST)  
    public JsonResult cacheReset(String cacheName) {  
        JsonResult jsonResult = new JsonResult();  
          
        GuavaCacheManager.resetCache(cacheName);  
        jsonResult.setMessage("已经成功重置了" + cacheName + "!");  
      
        return jsonResult;  
    }  
      
    /** 
     * 返回所有的本地缓存统计信息 
     * @return 
     */  
    @RequestMapping(value = "/stats/all", method = RequestMethod.POST)  
    public JsonResult cacheStatsAll() {  
        return cacheStats("*");  
    }  
      
    /** 
     * 分页查询数据详情 
     * @param pageSize 
     * @param pageNo 
     * @param cacheName 
     * @return 
     */  
    @RequestMapping(value = "/queryDataByPage", method = RequestMethod.POST)  
    public PageResult<Object> queryDataByPage(@RequestParam Map<String, String> params){  
        int pageSize = Integer.valueOf(params.get("pageSize"));  
        int pageNo = Integer.valueOf(params.get("pageNo"));  
        String cacheName = params.get("cacheName");  
          
        PageParams<Object> page = new PageParams<>();  
        page.setPageSize(pageSize);  
        page.setPageNo(pageNo);  
        Map<String, Object> param = new HashMap<>();  
        param.put("cacheName", cacheName);  
        page.setParams(param);  
          
        return GuavaCacheManager.queryDataByPage(page);  
    }  
}  

其他代码,包括测试页面和测试Controller请参考源代码: https://github.com/xujijun/MyJavaStudio ,有问题请留言。^_^

相关文章