Spring AOP和自定义注解实现Redis缓存(支持SPEL)

一、缓存简介

​ 在WEB应用中,很多请求总是会一遍遍地去获取一些相同的数据,因为这些数据是无状态的,所以当请求任务完成后,就会立马丢掉所获取的数据,在这些数据中,有些是需要时间去数据库获取,或者远程接口调用获取,或执行复杂运算得到。如果这部分数据变化不那么频繁,或者压根不会变化,我们就可以把这部分数据存入缓存中。缓存技术是提升网站性能的一大利器,有很多优秀的缓存框架如:Ehcache、Redis、Memcache等,这些框架都能帮助我们很好的实现数据缓存。

二、Spring Cache

​ Spring在3.1版本中引入了基于注解的缓存技术Spring Cache,它本质上不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,可以通过配置实现对多种缓存技术的集成,但是有个我认为比较大的缺陷,不能根据业务自定义缓存失效时间(貌似是这样?),下面我们采用自定义注解的方式,自己实现缓存,这里我们使用的缓存服务是Redis(Redis安装配置请自行百度…)

三、需求描述和关键点

​ 需求描述如下:查询时,先读取缓存,如果缓存中没有数据,则触发真正的数据获取,如果缓存中有数据,直接返回缓存中的数据;新增数据时,将数据写入缓存;删除数据时,删除对应的缓存数据。

​ 关键点:key的生成:查询/新增/删除,需要保证key的一致性

四、具体实现代码如下:

​ 4.1) 自定义注解

​ RedisCacheable:用于缓存读取

​ RedisCachePut:缓存写入

​ RedisCacheEvict:缓存清除

1
2
3
4
5
6
7
8
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RedisCacheable {
String[] cacheNames() default ""; //缓存名称,可以多个
String cacheKey(); //缓存key
int expire() default 28800; //有效期时间(单位:秒),默认8个小时
int reflash() default -1; //缓存主动刷新时间(单位:秒),默认不主动刷新
}
1
2
3
4
5
6
7
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface RedisCachePut {
String[] cacheNames() default ""; //缓存名称,可以多个
String cacheKey(); //缓存key
int expire() default 28800; //有效期时间(单位:秒),默认8个小时
}
1
2
3
4
5
6
7
@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表达式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Component
public 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实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
@Aspect
@Component()
public class RedisCacheableAspect {
private static Logger _loger = LoggerFactory.getLogger(RedisCacheableAspect.class);
@Resource
private RedisTemplate<String , Object> redisTemplate;
@Autowired
private 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框架,配置方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 缓存管理器
* @param redisTemplate
* @return
*/
@Bean
public 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) 给查询、添加、删除方法加上自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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) 查询测试

1
2
3
4
5
//执行查询
PaySecuryModel paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");
_loger.info("第一次查询返回数据:" + JSON.toJSONString(paySecuryModel));
paySecuryModel = paySecuryService.getPaySecuryByMchid("3400000001");
_loger.info("第二次查询返回数据:" + JSON.toJSONString(paySecuryModel));

查询1

可以看出,第二次查询没有去查数据库,redis缓存中也出现了这个缓存KEY:

1
2
3
127.0.0.1:6379> keys *
1) "RedisKey_CacheName_GetPaySecuryByMchId_CacheKey_3400000001"

​ 5.3) 添加测试

1
2
3
4
5
6
7
8
9
10
11
12
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));

查询1

通过控制台日志看到,在插入完数据后,执行查询并没有查数据库,而是直接读取缓存的,redis缓存中也出现了这个key:

1
2
127.0.0.1:6379> keys *
4) "RedisKey_CacheName_GetPaySecuryByMchId_CacheKey_12012012014"

​ 5.4) 清除测试

1
2
int i = paySecuryService.removePaySecuryByMchid("12012012014");
_loger.info("删除配置数据返回:" + i);

数据删除后,查看redis缓存中,已移除这条数据对应的key

六、结语

时间比较仓促,只是大概写好了一个架子,还有很多要优化的地方,各位朋友有好的思路和建议,也请留言探讨啊!!!

坚持原创技术分享,您的支持将鼓励我继续创作!
分享