基于 Filter 的简单缓存方案

需求:

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



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值