1.spring-context文件需要打开切面注解
<aop:aspectj-autoproxy proxy-target-class="true"/>
<context:annotation-config />
2.编写切面,如下,
package com.yeepay.g3.core.payplus.test.aspect;
import java.lang.reflect.Method;
import javax.annotation.Resource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.yeepay.g3.core.payplus.test.utils.MultiCache;
import com.yeepay.g3.core.payplus.test.utils.MultiCacheEvict;
import com.yeepay.g3.core.payplus.test.utils.RedisCacheBean;
@Component
@Aspect
public class CacheAspect {
@Resource
public RedisCacheBean redis;
/**
* 定义缓存逻辑
*/
@Around("@annotation(com.yeepay.g3.core.payplus.test.utils.MultiCache)")
public Object cache(ProceedingJoinPoint pjp ) {
Object result=null;
Boolean cacheEnable=true;
//判断是否开启缓存
if(!cacheEnable){
try {
result= pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return result;
}
Method method=getMethod(pjp);
MultiCache cacheable=method.getAnnotation(MultiCache.class);
String fieldKey =parseKey(cacheable.fieldKey(),method,pjp.getArgs());
//获取方法的返回类型,让缓存可以返回正确的类型
Class returnType=((MethodSignature)pjp.getSignature()).getReturnType();
//使用redis 的hash进行存取,易于管理
result= redis.hget(cacheable.key(), fieldKey,returnType);
if(result==null){
try {
result=pjp.proceed();
Assert.notNull(fieldKey);
redis.hset(cacheable.key(),fieldKey, result);
} catch (Throwable e) {
e.printStackTrace();
}
}
return result;
}
/*** 定义清除缓存逻辑*/
@Around(value="@annotation(com.yeepay.g3.core.payplus.test.utils.MultiCacheEvict)")
public Object evict(ProceedingJoinPoint pjp ){
Object result=null;
Boolean cacheEnable=true;
//判断是否开启缓存
if(!cacheEnable){
try {
result= pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return result;
}
Method method=getMethod(pjp);
MultiCacheEvict cacheable=method.getAnnotation(MultiCacheEvict.class);
String fieldKey =parseKey(cacheable.fieldKey(),method,pjp.getArgs());
//获取方法的返回类型,让缓存可以返回正确的类型
Class returnType=((MethodSignature)pjp.getSignature()).getReturnType();
//使用redis 的hash进行存取,易于管理
result= redis.hget(cacheable.key(), fieldKey);
if(result!=null){
redis.hdel(cacheable.key(), fieldKey);
//执行真正的删除,先更新数据库在更新缓存是最好的方案
try {
result=pjp.proceed();
} catch (Throwable e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return result;
}
/**
* 获取被拦截方法对象
*
* MethodSignature.getMethod() 获取的是顶层接口或者父类的方法对象
* 而缓存的注解在实现类的方法上
* 所以应该使用反射获取当前对象的方法对象
*/
public Method getMethod(ProceedingJoinPoint pjp){
//获取参数的类型
Object [] args=pjp.getArgs();
Class [] argTypes=new Class[pjp.getArgs().length];
for(int i=0;i<args.length;i++){
argTypes[i]=args[i].getClass();
}
Method method=null;
try {
method=pjp.getTarget().getClass().getMethod(pjp.getSignature().getName(),argTypes);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
}
return method;
}
/**
* 获取缓存的key
* key 定义在注解上,支持SPEL表达式
* @param pjp
* @return
*/
private String parseKey(String key,Method method,Object [] args){
//获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u =
new LocalVariableTableParameterNameDiscoverer();
String [] paraNameArr=u.getParameterNames(method);
//使用SPEL进行key的解析
ExpressionParser parser = new SpelExpressionParser();
//SPEL上下文
StandardEvaluationContext context = new StandardEvaluationContext();
//把方法参数放入SPEL上下文中
for(int i=0;i<paraNameArr.length;i++){
context.setVariable(paraNameArr[i], args[i]);
}
return parser.parseExpression(key).getValue(context,String.class);
}
}
3.注解如下:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MultiCache {
String key();
String fieldKey() ;
int expireTime() default 3600;
}
4.redis部分略
package com.yeepay.g3.core.payplus.test.utils;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONArray;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
@Component
public class RedisCacheBean {
//注入spring-redis配置好的bean,也可以在配置文件中定义这个RedisCacheBean,里面有一个属性jedisPool,并且属性有get set操作
@Resource
ShardedJedisPool jedisPool;
//private ShardedJedis jedis = jedisPool.getResource();
/**
* 把对象放入Hash中
*/
public void hset(String key,String field,Object o){
ShardedJedis jedis =jedisPool.getResource();
jedis.hset(key,field, JSONArray.toJSONString(o));
jedisPool.returnResource(jedis);
}
/**
* 从Hash中获取对象
*/
public String hget(String key,String field){
ShardedJedis jedis =jedisPool.getResource();
String text=jedis.hget(key, field);
jedisPool.returnResource(jedis);
return text;
}
/**
* 从Hash中获取对象,转换成制定类型
*/
public <T> T hget(String key,String field,Class<T> clazz){
String text=hget(key, field);
T result = JSONArray.parseObject(text, clazz);
return result;
}
/**
* 从Hash中删除对象
*/
public void hdel(String key,String ... field){
ShardedJedis jedis =jedisPool.getResource();
Object result=jedis.hdel(key,field);
jedisPool.returnResource(jedis);
}
}
5.service调用部分
//使用hset的方式,getMerchantById用作key,fieldKey当做map中的key
@MultiCache(key="getMerchantById",fieldKey="#name")
public MerchantEntity getMerchantById(String name) {
LOGGER.info("开始查询数据库");
return merchantDao.getMerchantById(name);
}
@MultiCacheEvict(key="getMerchantById",fieldKey="#name")
public void delMerchantById(String name) {
LOGGER.info("开始查询数据库");
merchantDao.delMerchantById(name);
}
主要流程是在执行真正的删除和更新数据库之前,进入切面,在进入redis做缓存赋值操作,
对于先删除缓存还是先删除数据库,建议先删除数据库,再删除缓存。
第一种情况先删缓存在删数据库:在多线程环境下,当一个线程把缓存删掉之后,另一个线程都缓存,都不到缓存就会直接读库,读到数据后就会更新缓存,先前的线程呢,才更新数据库,会造成缓存脏读的情况,很容易产生缓存脏读。
第二种情况先删数据库再删缓存,在多线程情况下,当一个线程删除数据库,另一个线程读取缓存数据,读到的是缓存的数据,当先前一个线程删完数据库后就会更新缓存,这是缓存就正常了,产生了一次脏读,并且方式一容易出现缓存击穿数据库压力大,并且当时一的缓存时间长,如果之后再也没有关于这条缓存的操作,数据库不存在,缓存却能得到是很可怕的。
参见:http://blog.youkuaiyun.com/baiyunpeng42/article/details/53813034