由于项目的实际情况,需要缓存一些比较不经常改动的数据在本地服务器中,以提高接口处理的速度。决定采用Guava Cache之后,整理了一些具体需求:
现在,该系统已经实现,并已经在正式环境中运行了一段时间,日均总命中次数超过一百万,大部分缓存的命中率在98%以上,为某些接口的请求节省了一半的时间。
Guava Cache简介:
Guava Cache提供了一种把数据(key-value对)缓存到本地(JVM)内存中的机制,适用于很少会改动的数据,比如地区信息、系统配置、字典数据,等。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。
本文介绍了一种对Guava LoadingCache的封装使用,并提供管理页面的实现。
首先,介绍一些Guava Cache的基本概念:
下面介绍对Guava Cache进行封装使用的具体方法:
各模块的具体代码,代码中已经包括了比较详尽的注解:
主要的依赖包:
com.google.guava
guava
18.0
/**
* 本地缓存接口
* @author XuJijun
*
* @param Key的类型
* @param Value的类型
*/
public interface ILocalCache {
/**
* 从缓存中获取数据
* @param key
* @return value
*/
public V get(K key);
}
package com.xjj.cache.guava;
/**
* 抽象Guava缓存类、缓存模板。
* 子类需要实现fetchData(key),从数据库或其他数据源(如Redis)中获取数据。
* 子类调用getValue(key)方法,从缓存中获取数据,并处理不同的异常,比如value为null时的InvalidCacheLoadException异常。
*
* @author XuJijun
* @Date 2015-05-18
*
* @param key 类型
* @param value 类型
*/
public abstract class GuavaAbstractLoadingCache {
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 cache;
/**
* 通过调用getCache().get(key)来获取数据
* @return cache
*/
public LoadingCache getCache() {
if(cache == null){ //使用双重校验锁保证只有一个cache实例
synchronized (this) {
if(cache == null){
cache = CacheBuilder.newBuilder().maximumSize(maximumSize) //缓存数据的最大条目,也可以使用.maximumWeight(weight)代替
.expireAfterWrite(expireAfterWriteDuration, timeUnit) //数据被创建多久后被移除
.recordStats() //启用统计
.build(new CacheLoader() {
@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;
}
}
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 implements ILocalCache {
//@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;
}
}
/**
* Area相关方法,使用缓存
* @author XuJijun
*
*/
@Service
public class AreaService implements IAreaService {
@Resource(name="LCAreaIdToArea")
ILocalCache lCAreaIdToArea;
/**
* 根据areaId获取Area
* @param areaId
* @return Area
*/
@Override
public Area getAreaById(int areaId) {
return lCAreaIdToArea.get(areaId);
}
}
package com.xjj.cache.guava;
/**
* Guava缓存监视和管理工具
* @author XuJijun
*
*/
public class GuavaCacheManager {
//保存一个Map: cacheName -> cache Object,以便根据cacheName获取Guava cache对象
private static Map> cacheNameToObjectMap = null;
/**
* 获取所有GuavaAbstractLoadingCache子类的实例,即所有的Guava Cache对象
* @return
*/
@SuppressWarnings("unchecked")
private static Map> getCacheMap(){
if(cacheNameToObjectMap==null){
cacheNameToObjectMap = (Map>) SpringContextUtil.getBeanOfType(GuavaAbstractLoadingCache.class);
}
return cacheNameToObjectMap;
}
/**
* 根据cacheName获取cache对象
* @param cacheName
* @return
*/
private static GuavaAbstractLoadingCache
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 queryDataByPage(@RequestParam Map params){
int pageSize = Integer.valueOf(params.get("pageSize"));
int pageNo = Integer.valueOf(params.get("pageNo"));
String cacheName = params.get("cacheName");
PageParams page = new PageParams<>();
page.setPageSize(pageSize);
page.setPageNo(pageNo);
Map param = new HashMap<>();
param.put("cacheName", cacheName);
page.setParams(param);
return GuavaCacheManager.queryDataByPage(page);
}
}
Cache Admin
Cache列表:
其他代码,包括测试页面和测试Controller请参考源代码:https://github.com/xujijun/MyJavaStudio,有问题请留言。^_^
测试页面:
管理页面:
(原创文章,转载请注明转自Clement-Xu的博客:http://blog.csdn.net/clementad/article/details/46491701,源码地址:https://github.com/xujijun/MyJavaStudio)