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.csdn.net/baiyunpeng42/article/details/53813034