Spring AOP和自定義註解實現Redis緩存(支持SPEL)

一、緩存簡介

在WEB應用中,很多請求總是會一遍遍地去獲取一些相同的數據,因為這些數據是無狀態的,所以當請求任務完成後,就會立馬丟掉所獲取的數據,在這些數據中,有些是需要時間去數據庫獲取,或者遠程接口調用獲取,或執行復雜運算得到。如果這部分數據變化不那麼頻繁,或者壓根不會變化,我們就可以把這部分數據存入緩存中。緩存技術是提升網站性能的一大利器,有很多優秀的緩存框架如:Ehcache、Redis、Memcache等,這些框架都能幫助我們很好的實現數據緩存。

二、Spring Cache

Spring在3.1版本中引入了基於註解的緩存技術Spring Cache,它本質上不是一個具體的緩存實現方案,而是一個對緩存使用的抽象,可以通過配置實現對多種緩存技術的集成,但是有個我認為比較大的缺陷,不能根據業務自定義緩存失效時間(貌似是這樣?),下面我們採用自定義註解的方式,自己實現緩存,這裡我們使用的緩存服務是Redis(Redis安裝配置請自行百度...)

三、需求描述和關鍵點

我們希望實現:查詢時,先讀取緩存,如果緩存中沒有數據,則觸發真正的數據獲取,如果緩存中有數據,直接返回緩存中的數據;新增數據時,將數據寫入緩存;刪除數據時,刪除對應的緩存數據。並且可以自定義每個KEY的緩存有效期。

關鍵點:key的生成:查詢/新增/刪除,需要保證key的一致性

四、具體實現代碼如下:

4.1) 自定義註解

RedisCacheable:用於緩存讀取

RedisCachePut:緩存寫入

RedisCacheEvict:緩存清除

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheable {String[] cacheNames() default ""; //緩存名稱,可以多個String cacheKey(); //緩存keyint expire() default 28800; //有效期時間(單位:秒),默認8個小時int reflash() default -1; //緩存主動刷新時間(單位:秒),默認不主動刷新}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCachePut { String[] cacheNames() default ""; //緩存名稱,可以多個 String cacheKey(); //緩存key int expire() default 28800; //有效期時間(單位:秒),默認8個小時}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheEvict { String[] cacheNames() default ""; //緩存名稱 String cacheKey(); //緩存key boolean allEntries() default false; //是否清空cacheName的全部數據}

4.2) key生成策略,緩存名+緩存KEY(支持Spring EL表達式)

@Componentpublic class DefaultKeyGenerator{ private static Logger _loger = LoggerFactory.getLogger(DefaultKeyGenerator.class); /** * @Description: redis key生成 * @param: cacheKey:key值必傳 ,cacheNames:緩存名稱,不傳取方法路徑 * @return: * @throws: * @author:pengl * @Date:2017/11/13 10:58 */ public String[] generateKey(ProceedingJoinPoint pjp, String[] cacheNames, String cacheKey) throws NoSuchMethodException { if(StringUtils.isEmpty(cacheKey)) throw new NullPointerException("CacheKey can not be null..."); Signature signature = pjp.getSignature(); if(cacheNames == null || cacheNames.length == 0){ cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } String[] results = new String[cacheNames.length]; //解析cacheKey EvaluationContext evaluationContext = new StandardEvaluationContext(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("This annotation can only be used for methods..."); } MethodSignature methodSignature = (MethodSignature) signature; //method參數列表 String[] parameterNames = methodSignature.getParameterNames(); Object[] args = pjp.getArgs(); for(int i = 0; i < parameterNames.length; i++){ String parameterName = parameterNames[i]; evaluationContext.setVariable(parameterName, args[i]); } for(int j = 0; j < cacheNames.length; j++){ results[j] = "RedisKey_CacheName_" + cacheNames[j] + "_CacheKey_" + new SpelExpressionParser().parseExpression(cacheKey).getValue(evaluationContext, String.class);//暫時只使用String類型 } _loger.info("=============>>>generateKeys : " + Arrays.toString(results)); return results; }}

4.3) 自定義緩存註解AOP實現

@Aspect@Component()public class RedisCacheableAspect {private static Logger _loger = LoggerFactory.getLogger(RedisCacheableAspect.class);@Resourceprivate RedisTemplate<String , Object> redisTemplate;@Autowiredprivate DefaultKeyGenerator defaultKeyGenerator;/** * @Description: 讀取緩存數據 * @param: * @return: * @throws: * @author: pengl * @Date: 2017/11/13 16:46 */@Around(value = "@annotation(cache)") public Object cached(final ProceedingJoinPoint pjp , RedisCacheable cache) throws Throwable { try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cache.cacheNames(), cache.cacheKey()); Object valueData = null; for(String key : keys){ //獲取緩存中的值 ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); byte[] value = (byte[]) valueOper.get(key); if(value != null){ //如果緩存有值,需要判斷刷新緩存設置和當前緩存的失效時間 int reflash = cache.reflash(); if(reflash > 0){ //查詢當前緩存失效時間是否在主動刷新規則範圍內 long exp = redisTemplate.getExpire(key,TimeUnit.SECONDS); if(exp <= reflash){ //主動刷新緩存,為不影響本次獲取效率,採用異步線程刷新緩存 } } return JDKSerializeUtil.unserialize(value); } //緩存中沒有值,執行實際數據查詢方法 if(valueData == null) valueData = pjp.proceed(); //寫入緩存 if(cache.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(valueData),cache.expire(),TimeUnit.SECONDS); //否則設置緩存時間 ,序列化存儲 } else { valueOper.set(key, JDKSerializeUtil.serialize(valueData)); } } return valueData; }catch(Exception e){ _loger.error("讀取Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 新增緩存 * @param: * @return: * @throws: * @author:pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cacheput)") public Object cachePut (final ProceedingJoinPoint pjp , RedisCachePut cacheput) throws Throwable{ try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cacheput.cacheNames(), cacheput.cacheKey()); Object valueData = pjp.proceed(); //寫入緩存 for(String key : keys){ ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); if(cacheput.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0]),cacheput.expire(),TimeUnit.SECONDS); } else { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0])); } } return valueData; }catch (Exception e){ _loger.error("寫入Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 刪除緩存 * @param: * @return: * @throws: * @author: pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cachevict)") public Object cacheEvict (final ProceedingJoinPoint pjp , RedisCacheEvict cachevict) throws Throwable{ try{ String[] cacheNames = cachevict.cacheNames(); boolean allEntries = cachevict.allEntries(); if(allEntries){ if(cacheNames == null || cacheNames.length == 0){ Signature signature = pjp.getSignature(); cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } for(String cacheName : cacheNames){ redisTemplate.delete(redisTemplate.keys("*" + "RedisKey_CacheName_" + cacheName + "*")); } }else{ String[] keys = defaultKeyGenerator.generateKey(pjp, cachevict.cacheNames(), cachevict.cacheKey()); for(String key : keys) redisTemplate.delete(key); } }catch (Exception e){ _loger.error("刪除Redis緩存失敗,異常信息:" + e.getMessage()); } return pjp.proceed(); }}

注意需要注入一個redisTemplate,這個根據環境不同採用不同的配置方式,我這裡使用的SpringBoot框架,配置方式如下:

@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport {/** * 緩存管理器 * @param redisTemplate * @return */@Beanpublic CacheManager cacheManager( @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { return new RedisCacheManager(redisTemplate);}/** * redis模板操作類 * @param factory * @return */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { final StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new JdkSerializationRedisSerializer()); return template; }}

五、測試

5.1) 給查詢、添加、刪除方法加上自定義註解

 @RedisCacheable(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid", expire = 3600) public PaySecuryModel getPaySecuryByMchid(String mchid){ return paySecuryMapper.findPaySecuryByMechid(mchid); }@RedisCachePut(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#paySecuryModel.mchid", expire = 3600) public int addPaySecury(PaySecuryModel paySecuryModel){ return paySecuryMapper.insertPaySecury(paySecuryModel); }@RedisCacheEvict(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid") public int removePaySecuryByMchid(String mchid){ return paySecuryMapper.deletePaySecuryByMchid(mchid); }

5.2) 查詢測試

//執行查詢PaySecuryModel paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第一次查詢返回數據:" + JSON.toJSONString(paySecuryModel));paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第二次查詢返回數據:" + JSON.toJSONString(paySecuryModel));

一、緩存簡介

在WEB應用中,很多請求總是會一遍遍地去獲取一些相同的數據,因為這些數據是無狀態的,所以當請求任務完成後,就會立馬丟掉所獲取的數據,在這些數據中,有些是需要時間去數據庫獲取,或者遠程接口調用獲取,或執行復雜運算得到。如果這部分數據變化不那麼頻繁,或者壓根不會變化,我們就可以把這部分數據存入緩存中。緩存技術是提升網站性能的一大利器,有很多優秀的緩存框架如:Ehcache、Redis、Memcache等,這些框架都能幫助我們很好的實現數據緩存。

二、Spring Cache

Spring在3.1版本中引入了基於註解的緩存技術Spring Cache,它本質上不是一個具體的緩存實現方案,而是一個對緩存使用的抽象,可以通過配置實現對多種緩存技術的集成,但是有個我認為比較大的缺陷,不能根據業務自定義緩存失效時間(貌似是這樣?),下面我們採用自定義註解的方式,自己實現緩存,這裡我們使用的緩存服務是Redis(Redis安裝配置請自行百度...)

三、需求描述和關鍵點

我們希望實現:查詢時,先讀取緩存,如果緩存中沒有數據,則觸發真正的數據獲取,如果緩存中有數據,直接返回緩存中的數據;新增數據時,將數據寫入緩存;刪除數據時,刪除對應的緩存數據。並且可以自定義每個KEY的緩存有效期。

關鍵點:key的生成:查詢/新增/刪除,需要保證key的一致性

四、具體實現代碼如下:

4.1) 自定義註解

RedisCacheable:用於緩存讀取

RedisCachePut:緩存寫入

RedisCacheEvict:緩存清除

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheable {String[] cacheNames() default ""; //緩存名稱,可以多個String cacheKey(); //緩存keyint expire() default 28800; //有效期時間(單位:秒),默認8個小時int reflash() default -1; //緩存主動刷新時間(單位:秒),默認不主動刷新}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCachePut { String[] cacheNames() default ""; //緩存名稱,可以多個 String cacheKey(); //緩存key int expire() default 28800; //有效期時間(單位:秒),默認8個小時}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheEvict { String[] cacheNames() default ""; //緩存名稱 String cacheKey(); //緩存key boolean allEntries() default false; //是否清空cacheName的全部數據}

4.2) key生成策略,緩存名+緩存KEY(支持Spring EL表達式)

@Componentpublic class DefaultKeyGenerator{ private static Logger _loger = LoggerFactory.getLogger(DefaultKeyGenerator.class); /** * @Description: redis key生成 * @param: cacheKey:key值必傳 ,cacheNames:緩存名稱,不傳取方法路徑 * @return: * @throws: * @author:pengl * @Date:2017/11/13 10:58 */ public String[] generateKey(ProceedingJoinPoint pjp, String[] cacheNames, String cacheKey) throws NoSuchMethodException { if(StringUtils.isEmpty(cacheKey)) throw new NullPointerException("CacheKey can not be null..."); Signature signature = pjp.getSignature(); if(cacheNames == null || cacheNames.length == 0){ cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } String[] results = new String[cacheNames.length]; //解析cacheKey EvaluationContext evaluationContext = new StandardEvaluationContext(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("This annotation can only be used for methods..."); } MethodSignature methodSignature = (MethodSignature) signature; //method參數列表 String[] parameterNames = methodSignature.getParameterNames(); Object[] args = pjp.getArgs(); for(int i = 0; i < parameterNames.length; i++){ String parameterName = parameterNames[i]; evaluationContext.setVariable(parameterName, args[i]); } for(int j = 0; j < cacheNames.length; j++){ results[j] = "RedisKey_CacheName_" + cacheNames[j] + "_CacheKey_" + new SpelExpressionParser().parseExpression(cacheKey).getValue(evaluationContext, String.class);//暫時只使用String類型 } _loger.info("=============>>>generateKeys : " + Arrays.toString(results)); return results; }}

4.3) 自定義緩存註解AOP實現

@Aspect@Component()public class RedisCacheableAspect {private static Logger _loger = LoggerFactory.getLogger(RedisCacheableAspect.class);@Resourceprivate RedisTemplate<String , Object> redisTemplate;@Autowiredprivate DefaultKeyGenerator defaultKeyGenerator;/** * @Description: 讀取緩存數據 * @param: * @return: * @throws: * @author: pengl * @Date: 2017/11/13 16:46 */@Around(value = "@annotation(cache)") public Object cached(final ProceedingJoinPoint pjp , RedisCacheable cache) throws Throwable { try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cache.cacheNames(), cache.cacheKey()); Object valueData = null; for(String key : keys){ //獲取緩存中的值 ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); byte[] value = (byte[]) valueOper.get(key); if(value != null){ //如果緩存有值,需要判斷刷新緩存設置和當前緩存的失效時間 int reflash = cache.reflash(); if(reflash > 0){ //查詢當前緩存失效時間是否在主動刷新規則範圍內 long exp = redisTemplate.getExpire(key,TimeUnit.SECONDS); if(exp <= reflash){ //主動刷新緩存,為不影響本次獲取效率,採用異步線程刷新緩存 } } return JDKSerializeUtil.unserialize(value); } //緩存中沒有值,執行實際數據查詢方法 if(valueData == null) valueData = pjp.proceed(); //寫入緩存 if(cache.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(valueData),cache.expire(),TimeUnit.SECONDS); //否則設置緩存時間 ,序列化存儲 } else { valueOper.set(key, JDKSerializeUtil.serialize(valueData)); } } return valueData; }catch(Exception e){ _loger.error("讀取Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 新增緩存 * @param: * @return: * @throws: * @author:pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cacheput)") public Object cachePut (final ProceedingJoinPoint pjp , RedisCachePut cacheput) throws Throwable{ try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cacheput.cacheNames(), cacheput.cacheKey()); Object valueData = pjp.proceed(); //寫入緩存 for(String key : keys){ ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); if(cacheput.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0]),cacheput.expire(),TimeUnit.SECONDS); } else { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0])); } } return valueData; }catch (Exception e){ _loger.error("寫入Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 刪除緩存 * @param: * @return: * @throws: * @author: pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cachevict)") public Object cacheEvict (final ProceedingJoinPoint pjp , RedisCacheEvict cachevict) throws Throwable{ try{ String[] cacheNames = cachevict.cacheNames(); boolean allEntries = cachevict.allEntries(); if(allEntries){ if(cacheNames == null || cacheNames.length == 0){ Signature signature = pjp.getSignature(); cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } for(String cacheName : cacheNames){ redisTemplate.delete(redisTemplate.keys("*" + "RedisKey_CacheName_" + cacheName + "*")); } }else{ String[] keys = defaultKeyGenerator.generateKey(pjp, cachevict.cacheNames(), cachevict.cacheKey()); for(String key : keys) redisTemplate.delete(key); } }catch (Exception e){ _loger.error("刪除Redis緩存失敗,異常信息:" + e.getMessage()); } return pjp.proceed(); }}

注意需要注入一個redisTemplate,這個根據環境不同採用不同的配置方式,我這裡使用的SpringBoot框架,配置方式如下:

@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport {/** * 緩存管理器 * @param redisTemplate * @return */@Beanpublic CacheManager cacheManager( @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { return new RedisCacheManager(redisTemplate);}/** * redis模板操作類 * @param factory * @return */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { final StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new JdkSerializationRedisSerializer()); return template; }}

五、測試

5.1) 給查詢、添加、刪除方法加上自定義註解

 @RedisCacheable(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid", expire = 3600) public PaySecuryModel getPaySecuryByMchid(String mchid){ return paySecuryMapper.findPaySecuryByMechid(mchid); }@RedisCachePut(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#paySecuryModel.mchid", expire = 3600) public int addPaySecury(PaySecuryModel paySecuryModel){ return paySecuryMapper.insertPaySecury(paySecuryModel); }@RedisCacheEvict(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid") public int removePaySecuryByMchid(String mchid){ return paySecuryMapper.deletePaySecuryByMchid(mchid); }

5.2) 查詢測試

//執行查詢PaySecuryModel paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第一次查詢返回數據:" + JSON.toJSONString(paySecuryModel));paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第二次查詢返回數據:" + JSON.toJSONString(paySecuryModel));

Spring AOP和自定義註解實現Redis緩存(支持SPEL)

可以看出,第二次查詢沒有去查數據庫,redis緩存中也出現了這個緩存KEY:

127.0.0.1:6379> keys *1) "RedisKey_CacheName_GetPaySecuryByMchId_CacheKey_3400000001"

5.3) 添加測試

PaySecuryModel paySecuryModel = new PaySecuryModel();paySecuryModel.setMchid("12012012014");paySecuryModel.setMchkey("testkey");paySecuryModel.setRiskcontrolinfo("");paySecuryModel.setMchpwd("testpwd");paySecuryModel.setWxappid("wx23238d971c6091");paySecuryModel.setWxsecret("f48232323cd426887bb67118dc37");int i = paySecuryService.addPaySecury(paySecuryModel);_loger.info("新增配置數據返回:" + i + ",主鍵ID:" + paySecuryModel.getId());//新增後查詢paySecuryModel = paySecuryService.getPaySecuryByMchid("12012012014"); _loger.info("新增查詢返回數據:" + JSON.toJSONString(paySecuryModel));

一、緩存簡介

在WEB應用中,很多請求總是會一遍遍地去獲取一些相同的數據,因為這些數據是無狀態的,所以當請求任務完成後,就會立馬丟掉所獲取的數據,在這些數據中,有些是需要時間去數據庫獲取,或者遠程接口調用獲取,或執行復雜運算得到。如果這部分數據變化不那麼頻繁,或者壓根不會變化,我們就可以把這部分數據存入緩存中。緩存技術是提升網站性能的一大利器,有很多優秀的緩存框架如:Ehcache、Redis、Memcache等,這些框架都能幫助我們很好的實現數據緩存。

二、Spring Cache

Spring在3.1版本中引入了基於註解的緩存技術Spring Cache,它本質上不是一個具體的緩存實現方案,而是一個對緩存使用的抽象,可以通過配置實現對多種緩存技術的集成,但是有個我認為比較大的缺陷,不能根據業務自定義緩存失效時間(貌似是這樣?),下面我們採用自定義註解的方式,自己實現緩存,這裡我們使用的緩存服務是Redis(Redis安裝配置請自行百度...)

三、需求描述和關鍵點

我們希望實現:查詢時,先讀取緩存,如果緩存中沒有數據,則觸發真正的數據獲取,如果緩存中有數據,直接返回緩存中的數據;新增數據時,將數據寫入緩存;刪除數據時,刪除對應的緩存數據。並且可以自定義每個KEY的緩存有效期。

關鍵點:key的生成:查詢/新增/刪除,需要保證key的一致性

四、具體實現代碼如下:

4.1) 自定義註解

RedisCacheable:用於緩存讀取

RedisCachePut:緩存寫入

RedisCacheEvict:緩存清除

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheable {String[] cacheNames() default ""; //緩存名稱,可以多個String cacheKey(); //緩存keyint expire() default 28800; //有效期時間(單位:秒),默認8個小時int reflash() default -1; //緩存主動刷新時間(單位:秒),默認不主動刷新}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCachePut { String[] cacheNames() default ""; //緩存名稱,可以多個 String cacheKey(); //緩存key int expire() default 28800; //有效期時間(單位:秒),默認8個小時}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheEvict { String[] cacheNames() default ""; //緩存名稱 String cacheKey(); //緩存key boolean allEntries() default false; //是否清空cacheName的全部數據}

4.2) key生成策略,緩存名+緩存KEY(支持Spring EL表達式)

@Componentpublic class DefaultKeyGenerator{ private static Logger _loger = LoggerFactory.getLogger(DefaultKeyGenerator.class); /** * @Description: redis key生成 * @param: cacheKey:key值必傳 ,cacheNames:緩存名稱,不傳取方法路徑 * @return: * @throws: * @author:pengl * @Date:2017/11/13 10:58 */ public String[] generateKey(ProceedingJoinPoint pjp, String[] cacheNames, String cacheKey) throws NoSuchMethodException { if(StringUtils.isEmpty(cacheKey)) throw new NullPointerException("CacheKey can not be null..."); Signature signature = pjp.getSignature(); if(cacheNames == null || cacheNames.length == 0){ cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } String[] results = new String[cacheNames.length]; //解析cacheKey EvaluationContext evaluationContext = new StandardEvaluationContext(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("This annotation can only be used for methods..."); } MethodSignature methodSignature = (MethodSignature) signature; //method參數列表 String[] parameterNames = methodSignature.getParameterNames(); Object[] args = pjp.getArgs(); for(int i = 0; i < parameterNames.length; i++){ String parameterName = parameterNames[i]; evaluationContext.setVariable(parameterName, args[i]); } for(int j = 0; j < cacheNames.length; j++){ results[j] = "RedisKey_CacheName_" + cacheNames[j] + "_CacheKey_" + new SpelExpressionParser().parseExpression(cacheKey).getValue(evaluationContext, String.class);//暫時只使用String類型 } _loger.info("=============>>>generateKeys : " + Arrays.toString(results)); return results; }}

4.3) 自定義緩存註解AOP實現

@Aspect@Component()public class RedisCacheableAspect {private static Logger _loger = LoggerFactory.getLogger(RedisCacheableAspect.class);@Resourceprivate RedisTemplate<String , Object> redisTemplate;@Autowiredprivate DefaultKeyGenerator defaultKeyGenerator;/** * @Description: 讀取緩存數據 * @param: * @return: * @throws: * @author: pengl * @Date: 2017/11/13 16:46 */@Around(value = "@annotation(cache)") public Object cached(final ProceedingJoinPoint pjp , RedisCacheable cache) throws Throwable { try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cache.cacheNames(), cache.cacheKey()); Object valueData = null; for(String key : keys){ //獲取緩存中的值 ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); byte[] value = (byte[]) valueOper.get(key); if(value != null){ //如果緩存有值,需要判斷刷新緩存設置和當前緩存的失效時間 int reflash = cache.reflash(); if(reflash > 0){ //查詢當前緩存失效時間是否在主動刷新規則範圍內 long exp = redisTemplate.getExpire(key,TimeUnit.SECONDS); if(exp <= reflash){ //主動刷新緩存,為不影響本次獲取效率,採用異步線程刷新緩存 } } return JDKSerializeUtil.unserialize(value); } //緩存中沒有值,執行實際數據查詢方法 if(valueData == null) valueData = pjp.proceed(); //寫入緩存 if(cache.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(valueData),cache.expire(),TimeUnit.SECONDS); //否則設置緩存時間 ,序列化存儲 } else { valueOper.set(key, JDKSerializeUtil.serialize(valueData)); } } return valueData; }catch(Exception e){ _loger.error("讀取Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 新增緩存 * @param: * @return: * @throws: * @author:pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cacheput)") public Object cachePut (final ProceedingJoinPoint pjp , RedisCachePut cacheput) throws Throwable{ try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cacheput.cacheNames(), cacheput.cacheKey()); Object valueData = pjp.proceed(); //寫入緩存 for(String key : keys){ ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); if(cacheput.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0]),cacheput.expire(),TimeUnit.SECONDS); } else { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0])); } } return valueData; }catch (Exception e){ _loger.error("寫入Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 刪除緩存 * @param: * @return: * @throws: * @author: pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cachevict)") public Object cacheEvict (final ProceedingJoinPoint pjp , RedisCacheEvict cachevict) throws Throwable{ try{ String[] cacheNames = cachevict.cacheNames(); boolean allEntries = cachevict.allEntries(); if(allEntries){ if(cacheNames == null || cacheNames.length == 0){ Signature signature = pjp.getSignature(); cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } for(String cacheName : cacheNames){ redisTemplate.delete(redisTemplate.keys("*" + "RedisKey_CacheName_" + cacheName + "*")); } }else{ String[] keys = defaultKeyGenerator.generateKey(pjp, cachevict.cacheNames(), cachevict.cacheKey()); for(String key : keys) redisTemplate.delete(key); } }catch (Exception e){ _loger.error("刪除Redis緩存失敗,異常信息:" + e.getMessage()); } return pjp.proceed(); }}

注意需要注入一個redisTemplate,這個根據環境不同採用不同的配置方式,我這裡使用的SpringBoot框架,配置方式如下:

@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport {/** * 緩存管理器 * @param redisTemplate * @return */@Beanpublic CacheManager cacheManager( @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { return new RedisCacheManager(redisTemplate);}/** * redis模板操作類 * @param factory * @return */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { final StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new JdkSerializationRedisSerializer()); return template; }}

五、測試

5.1) 給查詢、添加、刪除方法加上自定義註解

 @RedisCacheable(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid", expire = 3600) public PaySecuryModel getPaySecuryByMchid(String mchid){ return paySecuryMapper.findPaySecuryByMechid(mchid); }@RedisCachePut(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#paySecuryModel.mchid", expire = 3600) public int addPaySecury(PaySecuryModel paySecuryModel){ return paySecuryMapper.insertPaySecury(paySecuryModel); }@RedisCacheEvict(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid") public int removePaySecuryByMchid(String mchid){ return paySecuryMapper.deletePaySecuryByMchid(mchid); }

5.2) 查詢測試

//執行查詢PaySecuryModel paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第一次查詢返回數據:" + JSON.toJSONString(paySecuryModel));paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第二次查詢返回數據:" + JSON.toJSONString(paySecuryModel));

Spring AOP和自定義註解實現Redis緩存(支持SPEL)

可以看出,第二次查詢沒有去查數據庫,redis緩存中也出現了這個緩存KEY:

127.0.0.1:6379> keys *1) "RedisKey_CacheName_GetPaySecuryByMchId_CacheKey_3400000001"

5.3) 添加測試

PaySecuryModel paySecuryModel = new PaySecuryModel();paySecuryModel.setMchid("12012012014");paySecuryModel.setMchkey("testkey");paySecuryModel.setRiskcontrolinfo("");paySecuryModel.setMchpwd("testpwd");paySecuryModel.setWxappid("wx23238d971c6091");paySecuryModel.setWxsecret("f48232323cd426887bb67118dc37");int i = paySecuryService.addPaySecury(paySecuryModel);_loger.info("新增配置數據返回:" + i + ",主鍵ID:" + paySecuryModel.getId());//新增後查詢paySecuryModel = paySecuryService.getPaySecuryByMchid("12012012014"); _loger.info("新增查詢返回數據:" + JSON.toJSONString(paySecuryModel));

Spring AOP和自定義註解實現Redis緩存(支持SPEL)

通過控制檯日誌看到,在插入完數據後,執行查詢並沒有查數據庫,而是直接讀取緩存的,redis緩存中也出現了這個key:

127.0.0.1:6379> keys *4) "RedisKey_CacheName_GetPaySecuryByMchId_CacheKey_12012012014"

5.4) 清除測試

int i = paySecuryService.removePaySecuryByMchid("12012012014");_loger.info("刪除配置數據返回:" + i);

數據刪除後,查看redis緩存中,已移除這條數據對應的key

六、結語

時間比較倉促,只是大概寫好了一個架子,還有很多要優化的地方,各位朋友有好的思路和建議,也請留言探討啊!!!

一、緩存簡介

在WEB應用中,很多請求總是會一遍遍地去獲取一些相同的數據,因為這些數據是無狀態的,所以當請求任務完成後,就會立馬丟掉所獲取的數據,在這些數據中,有些是需要時間去數據庫獲取,或者遠程接口調用獲取,或執行復雜運算得到。如果這部分數據變化不那麼頻繁,或者壓根不會變化,我們就可以把這部分數據存入緩存中。緩存技術是提升網站性能的一大利器,有很多優秀的緩存框架如:Ehcache、Redis、Memcache等,這些框架都能幫助我們很好的實現數據緩存。

二、Spring Cache

Spring在3.1版本中引入了基於註解的緩存技術Spring Cache,它本質上不是一個具體的緩存實現方案,而是一個對緩存使用的抽象,可以通過配置實現對多種緩存技術的集成,但是有個我認為比較大的缺陷,不能根據業務自定義緩存失效時間(貌似是這樣?),下面我們採用自定義註解的方式,自己實現緩存,這裡我們使用的緩存服務是Redis(Redis安裝配置請自行百度...)

三、需求描述和關鍵點

我們希望實現:查詢時,先讀取緩存,如果緩存中沒有數據,則觸發真正的數據獲取,如果緩存中有數據,直接返回緩存中的數據;新增數據時,將數據寫入緩存;刪除數據時,刪除對應的緩存數據。並且可以自定義每個KEY的緩存有效期。

關鍵點:key的生成:查詢/新增/刪除,需要保證key的一致性

四、具體實現代碼如下:

4.1) 自定義註解

RedisCacheable:用於緩存讀取

RedisCachePut:緩存寫入

RedisCacheEvict:緩存清除

@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheable {String[] cacheNames() default ""; //緩存名稱,可以多個String cacheKey(); //緩存keyint expire() default 28800; //有效期時間(單位:秒),默認8個小時int reflash() default -1; //緩存主動刷新時間(單位:秒),默認不主動刷新}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCachePut { String[] cacheNames() default ""; //緩存名稱,可以多個 String cacheKey(); //緩存key int expire() default 28800; //有效期時間(單位:秒),默認8個小時}
@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.METHOD})public @interface RedisCacheEvict { String[] cacheNames() default ""; //緩存名稱 String cacheKey(); //緩存key boolean allEntries() default false; //是否清空cacheName的全部數據}

4.2) key生成策略,緩存名+緩存KEY(支持Spring EL表達式)

@Componentpublic class DefaultKeyGenerator{ private static Logger _loger = LoggerFactory.getLogger(DefaultKeyGenerator.class); /** * @Description: redis key生成 * @param: cacheKey:key值必傳 ,cacheNames:緩存名稱,不傳取方法路徑 * @return: * @throws: * @author:pengl * @Date:2017/11/13 10:58 */ public String[] generateKey(ProceedingJoinPoint pjp, String[] cacheNames, String cacheKey) throws NoSuchMethodException { if(StringUtils.isEmpty(cacheKey)) throw new NullPointerException("CacheKey can not be null..."); Signature signature = pjp.getSignature(); if(cacheNames == null || cacheNames.length == 0){ cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } String[] results = new String[cacheNames.length]; //解析cacheKey EvaluationContext evaluationContext = new StandardEvaluationContext(); if (!(signature instanceof MethodSignature)) { throw new IllegalArgumentException("This annotation can only be used for methods..."); } MethodSignature methodSignature = (MethodSignature) signature; //method參數列表 String[] parameterNames = methodSignature.getParameterNames(); Object[] args = pjp.getArgs(); for(int i = 0; i < parameterNames.length; i++){ String parameterName = parameterNames[i]; evaluationContext.setVariable(parameterName, args[i]); } for(int j = 0; j < cacheNames.length; j++){ results[j] = "RedisKey_CacheName_" + cacheNames[j] + "_CacheKey_" + new SpelExpressionParser().parseExpression(cacheKey).getValue(evaluationContext, String.class);//暫時只使用String類型 } _loger.info("=============>>>generateKeys : " + Arrays.toString(results)); return results; }}

4.3) 自定義緩存註解AOP實現

@Aspect@Component()public class RedisCacheableAspect {private static Logger _loger = LoggerFactory.getLogger(RedisCacheableAspect.class);@Resourceprivate RedisTemplate<String , Object> redisTemplate;@Autowiredprivate DefaultKeyGenerator defaultKeyGenerator;/** * @Description: 讀取緩存數據 * @param: * @return: * @throws: * @author: pengl * @Date: 2017/11/13 16:46 */@Around(value = "@annotation(cache)") public Object cached(final ProceedingJoinPoint pjp , RedisCacheable cache) throws Throwable { try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cache.cacheNames(), cache.cacheKey()); Object valueData = null; for(String key : keys){ //獲取緩存中的值 ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); byte[] value = (byte[]) valueOper.get(key); if(value != null){ //如果緩存有值,需要判斷刷新緩存設置和當前緩存的失效時間 int reflash = cache.reflash(); if(reflash > 0){ //查詢當前緩存失效時間是否在主動刷新規則範圍內 long exp = redisTemplate.getExpire(key,TimeUnit.SECONDS); if(exp <= reflash){ //主動刷新緩存,為不影響本次獲取效率,採用異步線程刷新緩存 } } return JDKSerializeUtil.unserialize(value); } //緩存中沒有值,執行實際數據查詢方法 if(valueData == null) valueData = pjp.proceed(); //寫入緩存 if(cache.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(valueData),cache.expire(),TimeUnit.SECONDS); //否則設置緩存時間 ,序列化存儲 } else { valueOper.set(key, JDKSerializeUtil.serialize(valueData)); } } return valueData; }catch(Exception e){ _loger.error("讀取Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 新增緩存 * @param: * @return: * @throws: * @author:pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cacheput)") public Object cachePut (final ProceedingJoinPoint pjp , RedisCachePut cacheput) throws Throwable{ try{ //生成緩存KEY String[] keys = defaultKeyGenerator.generateKey(pjp, cacheput.cacheNames(), cacheput.cacheKey()); Object valueData = pjp.proceed(); //寫入緩存 for(String key : keys){ ValueOperations<String, Object> valueOper = redisTemplate.opsForValue(); if(cacheput.expire() > 0) { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0]),cacheput.expire(),TimeUnit.SECONDS); } else { valueOper.set(key, JDKSerializeUtil.serialize(pjp.getArgs()[0])); } } return valueData; }catch (Exception e){ _loger.error("寫入Redis緩存失敗,異常信息:" + e.getMessage()); return pjp.proceed(); } } /** * @Description: 刪除緩存 * @param: * @return: * @throws: * @author: pengl * @Date:2017/11/13 17:09 */ @Around(value = "@annotation(cachevict)") public Object cacheEvict (final ProceedingJoinPoint pjp , RedisCacheEvict cachevict) throws Throwable{ try{ String[] cacheNames = cachevict.cacheNames(); boolean allEntries = cachevict.allEntries(); if(allEntries){ if(cacheNames == null || cacheNames.length == 0){ Signature signature = pjp.getSignature(); cacheNames = new String[]{signature.getDeclaringTypeName() + "." + signature.getName()}; } for(String cacheName : cacheNames){ redisTemplate.delete(redisTemplate.keys("*" + "RedisKey_CacheName_" + cacheName + "*")); } }else{ String[] keys = defaultKeyGenerator.generateKey(pjp, cachevict.cacheNames(), cachevict.cacheKey()); for(String key : keys) redisTemplate.delete(key); } }catch (Exception e){ _loger.error("刪除Redis緩存失敗,異常信息:" + e.getMessage()); } return pjp.proceed(); }}

注意需要注入一個redisTemplate,這個根據環境不同採用不同的配置方式,我這裡使用的SpringBoot框架,配置方式如下:

@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport {/** * 緩存管理器 * @param redisTemplate * @return */@Beanpublic CacheManager cacheManager( @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { return new RedisCacheManager(redisTemplate);}/** * redis模板操作類 * @param factory * @return */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { final StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new JdkSerializationRedisSerializer()); return template; }}

五、測試

5.1) 給查詢、添加、刪除方法加上自定義註解

 @RedisCacheable(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid", expire = 3600) public PaySecuryModel getPaySecuryByMchid(String mchid){ return paySecuryMapper.findPaySecuryByMechid(mchid); }@RedisCachePut(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#paySecuryModel.mchid", expire = 3600) public int addPaySecury(PaySecuryModel paySecuryModel){ return paySecuryMapper.insertPaySecury(paySecuryModel); }@RedisCacheEvict(cacheNames = {"GetPaySecuryByMchId"}, cacheKey = "#mchid") public int removePaySecuryByMchid(String mchid){ return paySecuryMapper.deletePaySecuryByMchid(mchid); }

5.2) 查詢測試

//執行查詢PaySecuryModel paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第一次查詢返回數據:" + JSON.toJSONString(paySecuryModel));paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");_loger.info("第二次查詢返回數據:" + JSON.toJSONString(paySecuryModel));

Spring AOP和自定義註解實現Redis緩存(支持SPEL)

可以看出,第二次查詢沒有去查數據庫,redis緩存中也出現了這個緩存KEY:

127.0.0.1:6379> keys *1) "RedisKey_CacheName_GetPaySecuryByMchId_CacheKey_3400000001"

5.3) 添加測試

PaySecuryModel paySecuryModel = new PaySecuryModel();paySecuryModel.setMchid("12012012014");paySecuryModel.setMchkey("testkey");paySecuryModel.setRiskcontrolinfo("");paySecuryModel.setMchpwd("testpwd");paySecuryModel.setWxappid("wx23238d971c6091");paySecuryModel.setWxsecret("f48232323cd426887bb67118dc37");int i = paySecuryService.addPaySecury(paySecuryModel);_loger.info("新增配置數據返回:" + i + ",主鍵ID:" + paySecuryModel.getId());//新增後查詢paySecuryModel = paySecuryService.getPaySecuryByMchid("12012012014"); _loger.info("新增查詢返回數據:" + JSON.toJSONString(paySecuryModel));

Spring AOP和自定義註解實現Redis緩存(支持SPEL)

通過控制檯日誌看到,在插入完數據後,執行查詢並沒有查數據庫,而是直接讀取緩存的,redis緩存中也出現了這個key:

127.0.0.1:6379> keys *4) "RedisKey_CacheName_GetPaySecuryByMchId_CacheKey_12012012014"

5.4) 清除測試

int i = paySecuryService.removePaySecuryByMchid("12012012014");_loger.info("刪除配置數據返回:" + i);

數據刪除後,查看redis緩存中,已移除這條數據對應的key

六、結語

時間比較倉促,只是大概寫好了一個架子,還有很多要優化的地方,各位朋友有好的思路和建議,也請留言探討啊!!!

Spring AOP和自定義註解實現Redis緩存(支持SPEL)

相關推薦

推薦中...