项目中缓存应用场景分析
- 以前项目采用hibernate的orm模型,memcached来做数据层的二级缓存,效果非常的好。现在互联网项目为充分利用数据库特征都采用mybatis来做orm框架。
- 项目中我们还是有一些缓存使用场景,spring提供了基于service方式拦截的缓存解决方案。org.springframework.cache.cache接口提供一些方法,spring还提供相应的实现类。详情可参考http://jinnianshilongnian.iteye.com/blog/2001040
- 为结合公司项目,决定自己来设计一套自己cache支持组件,采用redis来作为缓存工具,借鉴spring cache模型,使用aop methodInterceptor来实现service方法的缓存机制。
- spring cache设计是可crud操作维护数据缓存。我设计cache只是为减少数据查询,提高响应速度。使用场景适合数据不需要及时准备的展示。
定义CacheManager接口及方法
/**
* 缓存管理接口
*
* @author my
* @Date 2016年8月7日 下午4:11:21
*/
public interface CacheManager {
/**
* 得到缓存记录
* @author andrexu
* @param key
* @return
*/
public Serializable get(String key);
/**
* 写入一条记录到缓存中
* @author andrexu
* @param key 缓存key
* @param result 缓存内容
* @param timeToIdleSeconds 过期时间(单位秒)
*/
public boolean put(String key, Serializable result, int timeToIdleSeconds);
/**
* 写入一条记录到缓存中
* @author andrexu
* @param key 缓存key
* @param result 缓存内容
* @param idleDate 过期日期
*/
public boolean put(String key, Serializable result, Date idleDate);
/**
* 删除一条缓存
* @author andrexu
* @param key
*/
public void remove(String key);
/**
* 得到缓存数量
* @author andrexu
* @return
*/
public int getSize();
/**
* 清空缓存
* @author andrexu
*/
public void clear();
}
主要有get,put,rmove方法,put必须设置失效时间,失效时间点。
redis实现类RedisStrCacheManagerImpl
- redis比mecached作为cache优点,就不在概述。
- key采用String序列化。value选择json序列化相比jdk序列化速度快,体积小,在redisClient软件可以浏览缓存数据。
@Service("redisStrCacheManagerImpl")
public class RedisStrCacheManagerImpl implements CacheManager {
private final Logger logger = LoggerFactory.getLogger(RedisStrCacheManagerImpl.class);
public static final String PREFIX_KEY = "calm:cache:";
private RedisSerializer<String> keySerializer = new StringRedisSerializer();
// private RedisSerializer<Object> valueSerializer = new JdkSerializationRedisSerializer();
private RedisSerializer<Object> valueSerializer =new Jackson2JsonRedisSerializer<Object>(Object.class);
@Resource(name = "redisTemplate")
private RedisTemplate<Serializable, Object> redisTemplate;
@Override
public Serializable get(String cachekey) {
try {
final String cachekey2 = PREFIX_KEY + cachekey;
Object valueObject = redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[] key = keySerializer.serialize(cachekey2);
if (connection.exists(key)) {
byte[] value = connection.get(key);
return valueSerializer.deserialize(value);
}
return null;
}
});
if (valueObject == null) {
return null;
}
return (Serializable) valueObject;
} catch (Exception e) {
logger.error("RedisStrCacheManagerImpl:" + e.getMessage());
}
return null;
}
@Override
public boolean put(String key, final Serializable result, final int timeToIdleSeconds) {
try {
final String cachekey = PREFIX_KEY + key;
redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keyArray = keySerializer.serialize(cachekey);
connection.set(keyArray, valueSerializer.serialize(result));
//api里失效时间传 毫秒。
connection.pExpire(keyArray, TimeoutUtils.toMillis(timeToIdleSeconds, TimeUnit.SECONDS));
return null;
}
});
} catch (Exception e) {
logger.error("RedisStrCacheManagerImpl put:" + e.getMessage());
}
return true;
}
@Override
public boolean put(String key, final Serializable result, Date idleDate) {
try {
Date currentDate = new Date();
final long expireTime = idleDate.getTime() - currentDate.getTime();
final String cachekey = PREFIX_KEY + key;
redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keyArray = keySerializer.serialize(cachekey);
connection.set(keyArray, valueSerializer.serialize(result));
connection.pExpire(keyArray, expireTime);
return null;
}
});
} catch (Exception e) {
logger.error("RedisStrCacheManagerImpl put:" + e.getMessage());
}
return true;
}
@Override
public void remove(String key) {
final String cachekey = PREFIX_KEY + key;
redisTemplate.execute(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
byte[] keyArray = keySerializer.serialize(cachekey);
connection.del(keyArray);
return null;
}
});
}
@Override
public int getSize() {
if (true) {
throw new RuntimeException("not the method");
}
return 0;
}
@Override
public void clear() {
throw new RuntimeException("not the method");
}
在redisManager浏览cache数据:
MethodInterceptor详解
- 在controller调用service方式时,采用了MethodInterceptor方法来判断是否使用cache,查询cache。
- 根据Service方式上MethodCache注解来判断是否使用cache,及cache失效策略。
- 关注cache的key生成策略,参考了spring的DefaultKeyGenerator类,这里生成策略是className.methodName.arguments[0].arguments[1]的拼接字符串,然后再用Base64转码,参考代码方法getCacheKey
package com.calm.b.common.cache.methodcache;
import com.calm.b.common.cache.CacheManager;
import com.calm.b.common.cache.CacheUtils;
import com.calm.b.common.cache.anno.MethodCache;
import com.calm.b.util.Base64Utils;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import java.io.Serializable;
import java.lang.reflect.Method;
/**
*
* @author my
* @Date 2016年8月7日 下午4:09:35
*/
public class MethodCacheInterceptor implements MethodInterceptor {
private final Logger logger= LoggerFactory.getLogger(MethodCacheInterceptor.class);
@Resource(name="redisStrCacheManagerImpl")
private CacheManager memcachedManager;
@Value("${enable.method.cache}")
private String enableMethodCache;
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
String realClassName = StringUtils.substringBefore(invocation.getThis().toString(), "@");
Class<?> clazz = Class.forName(realClassName);
Method invocationMethod = invocation.getMethod();
Method method = clazz.getMethod(invocationMethod.getName(), invocationMethod.getParameterTypes());
if (method.getReturnType().getName().equals("void")) {
return invocation.proceed();
}
MethodCache methodCache = method.getAnnotation(MethodCache.class);
if (methodCache == null||methodCache.timeToIdleSeconds()<=0||!StringUtils.equals(enableMethodCache,"true")) {
return invocation.proceed();
}
String targetName = clazz.getName();
String methodName = method.getName();
/**
* 根据className,methodName,参数,及忽略参数,生产缓存key
*/
String cacheKey = getCacheKey(targetName, methodName, invocation.getArguments(), methodCache.ignoreParams());
Serializable cacheResult = memcachedManager.get(cacheKey);
if (cacheResult == null) {
cacheResult = (Serializable) invocation.proceed();
memcachedManager.put(cacheKey, cacheResult,
CacheUtils.calculateExpireDate(methodCache.idleTime(), methodCache.timeToIdleSeconds()));
}
return cacheResult;
}
/**
* key生成机制:className.methodName.arguments[0].arguments[1]. ... 再进行转码 若argument为领域对象,请重写其hashCode方法
*
* @param targetName
* @param methodName
* @param arguments
* @param ignoreParams
* @return
*/
private String getCacheKey(String targetName, String methodName, Object[] arguments, boolean ignoreParams) {
StringBuffer sb = new StringBuffer();
sb.append(targetName).append(".").append(methodName);
if (!ignoreParams) {
if ((arguments != null) && (arguments.length != 0)) {
for (int i = 0; i < arguments.length; i++) {
sb.append(".").append(arguments[i]);
}
}
}
return Base64Utils.encoding(sb.toString());
}
}
spring的proxy配置如下,采用spring proxy配置模式。
<!-- redis method cache component-->
<bean id="methodCacheInterceptor" lazy-init="false"
class="com.calm.b.common.cache.methodcache.MethodCacheInterceptor"/>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<value>*CacheService</value>
</property>
<property name="interceptorNames">
<list>
<value>methodCacheInterceptor</value>
</list>
</property>
</bean>
项目service方法使用:
@Override
@MethodCache(timeToIdleSeconds = 300)
public List<AuthorizedLogVo> findAuthorizedLogVos(Long tradeId){
// ignore business logic code
}