需求:
1. 为网站添加缓存,提升网站速度。
2. 不需要修改现有的任何代码
3. 缓存可管理控制
4. 缓存机制易于插拔
方案:
缓存目的——尽可能的减少系统对数据库的访问。
1. 因为不能修改系统原有代码,所以考虑做页面级的缓存。
2. 大体思路为:
使用 Filter 拦截客户端请求,然后根据自定义的拦截规则去匹配客户端的请求,
如果能匹配,则直接读取缓存数据响应客户端;此时如果缓存数据不存在,则拦截响应数据,将其加入缓存,然后响应客户端。
如果不匹配,则不需要缓存,按正常流程,响应客户端。
3. 由于网站是Tomcat集群架构,为了统一缓存数据,将缓存数据独立,增加一台缓存服务器Memcached Sever,缓存数据从 Memcached Server 中存取。
4. 提供缓存控制操作
A. 提供一个缓存总开关,随时控制缓存的开启和关闭。
B. 提供拦截规则的配置
关于拦截规则:
根据客户端的请求来缓存整个页面(请求的响应数据)。
有些页面与个人信息无关,可以只缓存一条对应的数据,为:全局的缓存。
有些页面与个人信息有关,每个用户看到的内容不一样,需要为每个用户缓存一条数据,为:个人缓存
拦截规则提供以下选项
1. 缓存总开关。(缓存前提条件)
2. 用户是否登录。(缓存条件:登录才缓存,不登录才缓存,均缓存)
3. 匹配的ULR,使用简单的通配符模式,便于操作。(缓存条件:匹配才缓存)
4. 缓存过期时间。(满足条件后,缓存的有效期)
5. 缓存的范围。(满足条件后,缓存的范围设置:全局,个人)
说明:该缓存策略,缓存粒度太大,不易重用,冗余度高,缓存数据会很大(个人缓存)。这些都导致了,缓存命中率低,尤其在缓存过期时间比较短的情况下,缓存命中率更低。
在不改动现有代码的前提下,暂时只想到了这么做。
简单架构示意图和流程图:
关于Memcached 缓存服务器 可以参考:http://tech.idv2.com/2008/07/10/memcached-001/ (Memcached完全剖析)
核心代码部分:
package cache.bean;
import prx.core.security.MD5;
public class CacheKey {
private int login; //登录状态
private String userId; //登录的用户ID
private String url; //请求的URL
private int cacheTime; //缓存过期(毫秒)--从匹配的缓存规则中取得
private int cacheScope; //缓存范围 --从匹配的缓存规则中取得
/**
* 取得组合Key
* @return
*/
public String getKey() {
String key = url;
//个人范围的缓存需要加入 userId 信息,使不同的人访问相同页面有不同的缓存
if(userId != null && cacheScope == CacheConfig.SCOPE_MEMBER) {
key = userId + url;
}
return MD5.getHashString(key);
}
// getter and setter
}
package cache.bean;
/**
* 缓存规则
* @author PRX
*/
public class CacheConfig implements Serializable {
/** 登录或未登录 */
public static final int LOGIN_ALL = 0;
/** 登录 */
public static final int LOGIN_IN = 1;
/** 未登录 */
public static final int LOGIN_OUT = 2;
/** 全局范围 */
public static final int SCOPE_ALL = 1;
/** 个人范围 */
public static final int SCOPE_MEMBER = 2;
/** 缓存开关 */
public static boolean cacheSwitch = true;
private String cacheId; //缓存规则ID
private String url; //给定URL正则表达式
private int login; //0:登不登录都缓存,1:只有登录后才缓存,2:中有未登录才缓存
/**
* 缓存范围 1: 全局 2: 个人
* 指:登录后,页面数据是针对个人(每个人都不同),还是全局(每个人均相同)
* 用户必须为登录状态,个人范围才有效
*/
private int cacheScope;
private int cacheTime; //缓存过期时间(毫秒)
// getter and setter
}
package cache.filter;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* 响应数据包装类,用于拦截响应数据(HTML代码)
* @author PRX
*/
public class ResponseWrapper extends HttpServletResponseWrapper {
private ByteArrayOutputStream bufferedWriter;
private PrintWriter tmpWriter;
public ResponseWrapper(HttpServletResponse response) {
super(response);
//用于保存响应数据的输出流
bufferedWriter = new ByteArrayOutputStream();
//bufferedWriter的输入流包装,通过tmpWriter往bufferedWriter写入数据
tmpWriter = new PrintWriter(bufferedWriter);
}
/**
* 修改响应数据输入流为tmpWriter
*/
public PrintWriter getWriter() {
// return servletResponse.getWriter();
return tmpWriter;
}
/**
* 取得拦截的响应数据
* @param charset 数据编码
* @return
*/
public String getContent(String charset) {
try {
String result = bufferedWriter.toString(charset);
return result;
} catch (UnsupportedEncodingException e) {
return "UnsupportedEncoding";
}
}
}
package cache.filter;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.lang.StringUtils;
import prx.core.string.StringUtil;
import prx.dao.service.DBService;
import prx.cache.MemcacheUtil;
import cache.bean.CacheConfig;
import cache.bean.CacheKey;
public class CacheUtils {
/**
* 分析请求的URL,包装为CacheKey
* @param request
* @return
*/
public static CacheKey getCacheKeyBeanFromURL(HttpServletRequest request) {
CacheKey cacheKey = new CacheKey();
String userId = getLoginUserId(request);
if(StringUtils.isNotEmpty(userId)) { //已登录
cacheKey.setLogin(CacheConfig.LOGIN_IN);
cacheKey.setUserId(userId);
} else { //未登录
cacheKey.setLogin(CacheConfig.LOGIN_OUT);
}
cacheKey.setUrl(request.getRequestURI());
return cacheKey;
}
/**
* 判断用户是否登录
* @param request
* @return userId
*/
private static String getLoginUserId(HttpServletRequest request) {
String userid = (String)request.getSession().getAttribute("userid");
if(StringUtils.isNotEmpty(userid)) {
return userid;
} else {
return null;
}
}
/**
* 根据缓存配置CacheConfig判断该CacheKey是否需要缓存
* @param cacheKey
* @return
*/
public static boolean isNeedCache(CacheKey cacheKey) {
//若缓存总开关为关闭状态,则直接返回不缓存
if(CacheConfig.cacheSwitch == false) {
return false;
}
//从缓存中取得配置数据
List<CacheConfig> cacheConfigList = MemcacheUtil.get("cacheConfigList");
//若还没有缓存,则从数据库去数据,然后加入缓存
if(cacheConfigList == null) {
cacheConfigList = DBService.queryForList("select * from T_CACHE_CONFIG", CacheConfig.class);
MemcacheUtil.set("cacheConfigList", 0, cacheConfigList); // 0: 表示缓存不到期,一直存在
}
for(CacheConfig cacheConfig : cacheConfigList) {
//如果缓存符合配置的登录条件,则继续判断
if(cacheConfig.getLogin() == CacheConfig.LOGIN_ALL || cacheConfig.getLogin() == cacheKey.getLogin()) {
//判断CacheKey的URL是否与配置中的URL正则表达式匹配
if(StringUtil.patternMatcher(cacheConfig.getUrl(), cacheKey.getUrl())) {
//符合缓存配置,加入配置中的缓存范围和缓存时间
cacheKey.setCacheScope(cacheConfig.getCacheScope());
cacheKey.setCacheTime(cacheConfig.getCacheTime());
return true;
}
}
}
return false;
}
/**
* 根据CacheKey取得缓存数据,若缓存过期则返回null
* @param <T>
* @param cacheKey
* @return
*/
public static <T> T getCacheData(CacheKey cacheKey) {
return MemcacheUtil.get(cacheKey.getKey());
}
/**
* 根据CacheKey存取给定的数据
* @param cacheKey
* @param value
* @return
*/
public static boolean setCacheData(CacheKey cacheKey, Object value) {
return MemcacheUtil.set(cacheKey.getKey(), cacheKey.getCacheTime(), value);
}
}
package cache.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang.StringUtils;
import cache.bean.CacheKey;
public class CacheFilter implements Filter {
private String charset = "utf-8"; //编码方式
public void destroy() {
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
//分析请求URL,包装为CacheKey
CacheKey cacheKey = CacheUtils.getCacheKeyBeanFromURL(httpRequest);
//是否需要缓存
if(CacheUtils.isNeedCache(cacheKey)) {
//取得缓存数据
String responseData = CacheUtils.getCacheData(cacheKey);
if(StringUtils.isNotEmpty(responseData)) {
//缓存存在,直接使用缓存数据响应客户端
httpResponse.setCharacterEncoding(charset);
httpResponse.setContentType("text/html");
httpResponse.getWriter().print(responseData);
} else {
//缓存不存在,拦截响应数据,加入缓存
ResponseWrapper warpper = new ResponseWrapper(httpResponse);
chain.doFilter(request, warpper);
warpper.flushBuffer();
responseData = warpper.getContent(charset);
//将数据加入缓存
CacheUtils.setCacheData(cacheKey, responseData);
}
} else {
//不需要缓存,直接到下一步
chain.doFilter(request, response);
}
}
public void init(FilterConfig config) throws ServletException {
String param = config.getInitParameter("charset");
if(StringUtils.isNotBlank(param)) {
charset = param;
}
}
}
Web.xml 配置
<!-- 增加的缓存配置start -->
<filter>
<filter-name>Cache</filter-name>
<filter-class>cache.filter.CacheFilter</filter-class>
<init-param>
<param-name>charset</param-name>
<param-value>gbk</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Cache</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 缓存配置end -->