由于项目的实际情况,需要缓存一些比较不经常改动的数据在本地服务器中,以提高接口处理的速度。决定采用Guava Cache之后,整理了一些具体需求:
现在,该系统已经实现,并已经在正式环境中运行了一段时间,日均总命中次数超过一百万,大部分缓存的命中率在98%以上,为某些接口的请求节省了一半的时间。
Guava Cache简介:
Guava Cache提供了一种把数据(key-value对)缓存到本地(JVM)内存中的机制,适用于很少会改动的数据,比如地区信息、系统配置、字典数据,等。Guava Cache与ConcurrentMap很相似,但也不完全一样。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显式地移除。相对地,Guava Cache为了限制内存占用,通常都设定为自动回收元素。
本文介绍了一种对Guava LoadingCache的封装使用,并提供管理页面的实现。
首先,介绍一些Guava Cache的基本概念:
下面介绍对Guava Cache进行封装使用的具体方法:
各模块的具体代码,代码中已经包括了比较详尽的注解:
主要的依赖包:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency>
/** * 本地缓存接口 * @author XuJijun * * @param <K> Key的类型 * @param <V> Value的类型 */ public interface ILocalCache <K, V> { /** * 从缓存中获取数据 * @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 <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; } }
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; } }
/** * 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); } }
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; } }
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); } }
<!DOCTYPE html> <html> <head> <title>Cache Admin</title> <meta charset="UTF-8"> <script src="./resources/js/jquery-2.1.4.js" charset="UTF-8" type="text/javascript"></script> <style> .important {color : red;} .attention {color : orange;} .perfect {color : green;} .highlight {color : blue;} table{border: 1px solid #8968CD; border-collapse: collapse;} th,td{border: 1px solid #8968CD; padding:6px;} td{color: green;} </style> </head> <body> <div> <div id="operations"> Cache列表: <input type="button" value="刷新" onClick="refreshStatsAll();"> <input type="checkbox" id="autoRefresh" onClick="toggleAutoRefreshStats();"><label for="autoRefresh">自动刷新(3s)</label> </div> <div><pre id="response" class="attention"></pre></div> <div><br><pre id="responseRawData" ></pre></div> </div> </body> <script> var autoRefreshInterval = 3000; var autoRefershObject; var requestStatsAll = {url : "/cache/admin/stats/all", params : "*", callback: requestStatsAllCallback}; $(function() { refreshStatsAll(); }); function refreshStatsAll(){ ajaxRequest(requestStatsAll.url, requestStatsAll.params, requestStatsAll.callback); } function sizeStatistics(obj){ var c = "当前数据量/上限:" + obj.size + "/" + obj.maximumSize; c += "\n历史最高数据量:" + obj.highestSize; c += "\n最高数据量时间:" + obj.highestTime; return c; } function hitStatistics(obj){ var c = "命中数量:" + obj.hitCount; c += "\n命中比例:" + obj.hitRate; c += "\n读库比例:" + obj.missRate; return c; } function loadStatistics(obj){ var c = "成功加载数:" + obj.loadSuccessCount; c += "\n失败加载数:" + obj.loadExceptionCount; c += "\n总加载毫秒:" + obj.totalLoadTime; return c; } function requestStatsAllCallback(jsonResult){ var html = "<table><tr><th>Cache名称</th> <th>数据量统计</th> <th>命中统计</th> <th>加载统计</th> <th>开始/重置时间</th> <th>操作</th> </tr>"; $.each(jsonResult.data, function(idx, obj){ html += "<tr><th>" + obj.cacheName + "</th>" + "<td>" + sizeStatistics(obj) + "</td>" + "<td>" + hitStatistics(obj) + "</td>" + "<td>" + loadStatistics(obj) + "</td>" + "<td>" + obj.resetTime +"\n\n失效时长:" + obj.survivalDuration + "(分钟)</td>" + "<td>" + "<a href='javascript:void(0)' onclick='resetCache(\""+obj.cacheName+"\");'>清空缓存</a>" + "\t<a href='javascript:void(0)' onclick='queryDataByPage(\""+obj.cacheName+"\");'>显示详情</a>" + "</td>" + "</tr>"; }); html += "</table>"; $("#response").html(html); } function resetCache(cacheName){ $.ajax({ type : "POST", url : getRootPath()+"/cache/admin/reset", dataType : "json", //表示返回值类型 data : {"cacheName":cacheName}, success : function(jsonResult){alert(jsonResult.message);refreshStatsAll();} }); } //定时刷新开关 function toggleAutoRefreshStats(){ if($("#autoRefresh").prop("checked")==true){ autoRefershObject = setInterval(refreshStatsAll, autoRefreshInterval); }else{ clearInterval(autoRefershObject); } } var pageParam = {pageNo : 1, pageSize : 10, cacheName : null}; function resetpageParam(){ pageParam.pageNo = 1; pageParam.totalPage = 0; } function queryDataByPage(cacheName){ resetpageParam(); pageParam.cacheName = cacheName; ajaxRequest("/cache/admin/queryDataByPage", pageParam, pageQueryCallback); } function pageQueryCallback(jsonResult){ pageParam.totalPage = jsonResult.totalPage; var html = "<label class='highlight'>Cache名称:" + pageParam.cacheName + "</label><br/><br/>"; html += "<a href='javascript:void(0)' onclick='firstPage();'>首页 </a>\t"; html += "<a href='javascript:void(0)' onclick='previousPage();'>上一页 </a>\t"; html += "第<input type='number' id='pageNo' min='1' max='" + jsonResult.totalPage + "' value='" + jsonResult.pageNo + "' size='" + lengthOfNum(jsonResult.totalPage) + "' />页(共" + jsonResult.totalPage + "页)\t"; html += "<a href='javascript:void(0)' onclick='nextPage();'>下一页 </a>\t"; html += "<a href='javascript:void(0)' onclick='lastPage();'>末页</a>\t"; html += "<br/><br/>"; html += JSON.stringify(jsonResult.results[0], null, "\t"); $("#responseRawData").html(html); $("#pageNo").blur(function(){ pn = $("#pageNo").val(); if(pn < 1){ pn = 1; $("#pageNo").val(pn); }else if(pn > pageParam.totalPage){ pn = pageParam.totalPage; $("#pageNo").val(pn); } pageParam.pageNo=pn; ajaxRequest("/cache/admin/queryDataByPage", pageParam, pageQueryCallback); }); //回车 $("#pageNo").keyup(function(event){ if(event.which != 13){ return; } pn = $("#pageNo").val(); if(pn < 1){ pn = 1; $("#pageNo").val(pn); }else if(pn > pageParam.totalPage){ pn = pageParam.totalPage; $("#pageNo").val(pn); } pageParam.pageNo=pn; ajaxRequest("/cache/admin/queryDataByPage", pageParam, pageQueryCallback); }); } function firstPage(){ pageParam.pageNo=1; ajaxRequest("/cache/admin/queryDataByPage", pageParam, pageQueryCallback); } function lastPage(){ pageParam.pageNo=pageParam.totalPage; ajaxRequest("/cache/admin/queryDataByPage", pageParam, pageQueryCallback); } function nextPage(){ if(pageParam.pageNo==pageParam.totalPage){ alert("已经是最后一页了!"); return; } pageParam.pageNo++; ajaxRequest("/cache/admin/queryDataByPage", pageParam, pageQueryCallback); } function previousPage(){ if(pageParam.pageNo==1){ alert("已经是第一页了!"); return; } pageParam.pageNo--; ajaxRequest("/cache/admin/queryDataByPage", pageParam, pageQueryCallback); } //js获取项目根路径,如: http://localhost:8083/uimcardprj function getRootPath() { //获取当前网址,如: http://localhost:8083/uimcardprj/share/meun.jsp var curWwwPath = window.document.location.href; //获取主机地址之后的目录,如: uimcardprj/share/meun.jsp var pathName = window.document.location.pathname; var pos = curWwwPath.indexOf(pathName); //获取主机地址,如: http://localhost:8083 var localhostPath = curWwwPath.substring(0, pos); //获取带"/"的项目名,如:/uimcardprj var projectName = pathName.substring(0, pathName.substr(1).indexOf('/') + 1); return (localhostPath + projectName); } //发送ajax请求 function ajaxRequest(url, params, successCallback, contentType, errorCallback, async) { var _async = async || true; $.ajax({ type : "POST", url : getRootPath() + url, async : _async, contentType : contentType, dataType : "json", //表示返回值类型 data : params, success : successCallback, error : errorCallback }); } function lengthOfNum(num){ var length = 1; var _num = num; while((_num=_num/10) >= 1){ length++; } return length; } </script> </html>
其他代码,包括测试页面和测试Controller请参考源代码:https://github.com/xujijun/MyJavaStudio,有问题请留言。^_^
测试页面:
管理页面:
(原创文章,转载请注明转自Clement-Xu的博客:http://blog.csdn.net/clementad/article/details/46491701,源码地址:https://github.com/xujijun/MyJavaStudio)