【原】Spring AOP实现对Redis的缓存同步
前言:刚开始采用spring cache作为缓存数据,到后面发现扩展性不灵活,于是基于sprig cache原理自定义一套规则用于缓存数据。
请求过程:
- 根据请求参数生成Key,后面我们会对生成Key的规则,进一步说明;
- 根据Key去缓存服务器中取数据,如果取到数据,则返回数据,如果没有取到数据,则执行service中的方法调用dao从DB中获取数据,同时成功后将数据放到缓存中。
- 删除、新增、修改会触发更新缓存的拦截类对缓存服务器进行更新。
1.首先贴上核心注解类
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface RedisLogService { enum CACHE_OPERATION {
FIND, // 查询缓存操作
UPDATE, // 需要执行修改缓存的操作
INSERT; // 需要执行新增缓存的操作
} /** 存储的分组 */
String[] group(); /** 当前缓存操作类型 */
CACHE_OPERATION cacheOperation() default CACHE_OPERATION.FIND; /** 存储的Key 默认加入类名跟方法名 */
String key() default ""; /** 是否使用缓存 */
boolean use() default true; /** 超时时间 */
int expire() default 0; enum LOG_OPERATION {
ON, // 开启日志记录
OFF, // 关闭日志记录
} /** 当前缓存操作类型 */
LOG_OPERATION logOperation() default LOG_OPERATION.ON; /** 操作名称 */
String name() default ""; /** 操作参数 */
String param() default ""; /** 日志参数 操作人操作IP,操作IP归属地 */
String logParam() default "";
2.使用注解案例。
@RedisLogService(group = {
"group.news" }, key = "#record", name = "网站维护-公司新闻管理-分页查询公司新闻", param = "#record", logParam = "#map")
解释下上面注解:根据业务的需要,将缓存key进行分组,第一个group参数即是分组,用来标识某个模块,例如新闻模块统一是group.news;第二个key是根据参数拼接成的key,第三个name只是一个名称而已,没什么太大的作用,主要是用于给其它开发人员理解, 第四个param则是操作参数,这个很重要,到时候会用它来拼接key,第五个logParam是日志。
3.贴上具体拦截类
@Aspect
@Order(value = 1)
@Component("redisLogServiceInterceptor")
public class RedisLogServiceInterceptor { private static final Logger LOGGER = LoggerFactory.getLogger(RedisLogServiceInterceptor.class); @Autowired
private UserLogRecordService userLogRecordService; @Autowired
private RedisTemplate<String, Object> redisTemplate; /**
*
*
* @Title: execute
* @Description: 切入点业务逻辑
* @param proceedingJoinPoint
* @return
*/
@Around("@annotation(RedisLogService)")
public Object execute(ProceedingJoinPoint proceedingJoinPoint) throws ServiceException {
Object result = null; try {
Method method = getMethod(proceedingJoinPoint); // 获取注解对象
RedisLogService redisLogService = method.getAnnotation(RedisLogService.class); // 判断是否使用缓存
boolean useRedis = redisLogService.use(); if (useRedis) { // 使用redis
ValueOperations<String, Object> operations = redisTemplate.opsForValue(); // 判断当前操作
switch (redisLogService.cacheOperation()) { case FIND: result = executeDefault(redisLogService, operations, proceedingJoinPoint, method); break;
case UPDATE: result = executeUpdate(redisLogService, operations, proceedingJoinPoint); break;
case INSERT: result = executeInsert(redisLogService, operations, proceedingJoinPoint); break;
default: result = proceedingJoinPoint.proceed(); break;
}
} else { result = proceedingJoinPoint.proceed();
} } catch (ServiceException e) {
throw e;
} catch (Throwable e) {
throw new ServiceException(new Result<Object>("500", e.getMessage()), e);
}
return result;
} /**
*
* @Title: getMethod
* @Description: 获取被拦截方法对象
* @param joinPoint
* @return
*/
protected Method getMethod(JoinPoint joinPoint) throws Exception { MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); return method;
}
上面的代码使用了@Around环绕切面这个注解,为什么不用@Befor或者@After呢?
由于@Befor是在方法执行开始前才进行切面,而@After是方法结束后进行切面。 根据业务场景的需要,@Around 可以在所拦截方法的前后执行一段逻辑,例如在查询前先去Redis查数据,发现没有数据再回到service层去执行查db,查完了之后需要把数据重新放到Redis,此时其他线程的请求就可以直接从Redis获得数据,减少频繁对数据库的操作。
4.下面贴上查询的具体实现方法
/**
*
* @Title: executeDefault
* @Description: 默认操作的执行
* @param redisLogService
* @param result
* @param operations
* @param proceedingJoinPoint
* @param method
* @throws Throwable
*/
@SuppressWarnings("unchecked")
private Object executeDefault(RedisLogService redisLogService, ValueOperations<String, Object> operations,
ProceedingJoinPoint proceedingJoinPoint, Method method) throws Throwable { Object result = null; Object[] args = proceedingJoinPoint.getArgs(); // 获取被拦截方法参数名列表(使用Spring支持类库)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paraNameArr = u.getParameterNames(method); // 获取key的后缀的参数名
String key = redisLogService.key(); if (StringUtils.isNotBlank(key)) {
// 使用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]);
} Object object = parser.parseExpression(key).getValue(context); if (null != object) { if (object instanceof Map<?, ?>) { key = GzdtlStringUtil.transMapToString((Map<String, Object>) object); } else if (object instanceof Collection<?>) { Collection<Object> collection = (Collection<Object>) object; StringBuffer stringBuffer = new StringBuffer(); for (Object o : collection) { stringBuffer.append(o.toString());
} key = stringBuffer.toString();
} else { key = object.toString();
}
}
} String className = proceedingJoinPoint.getTarget().getClass().getName(); if (className.indexOf(".") >= 0) { className = className.substring(className.lastIndexOf(".") + 1, className.length());
} String methodName = method.getName(); String[] group = redisLogService.group(); if (null != group && group.length > 0) { if (StringUtils.isNotBlank(key)) { key = group[0] + ":" + className + ":" + methodName + ":" + key;
} else { key = group[0] + ":" + className + ":" + methodName;
}
} else { if (StringUtils.isNotBlank(key)) { key = "group" + ":" + className + ":" + methodName + ":" + key;
} else { key = "group" + ":" + className + ":" + methodName;
}
} result = operations.get(key); // 如果缓存没有数据则更新缓存
if (result == null) { result = proceedingJoinPoint.proceed(); int expire = redisLogService.expire(); // 更新缓存
if (expire > 0) { operations.set(key, result, expire, TimeUnit.SECONDS);
} else { operations.set(key, result);
}
} return result;
}
proceedingJoinPoint.getArgs() 作用:
了解过aop 以及反射相关技术的都知道这是从方法内取出传入参数,例如传入的是 (String user,String age), 通过这个方法可以分别得到user和age的值。
例如如下代码块:
public Result<PageInfo<WebInfoBase>> findPageByParam(WebInfoFindParam record, Map<String, String> map)
// 从paraNameArr获取参数的别名分别是record和map
String[] paraNameArr = u.getParameterNames(method);
分析完毕后举个请求的例子
假设用户id = 1,分页查询了订单信息,这时候 record 参数为:pageSize:10,pageNum:2,id:1。key的最终格式 : group+namespace+record(这样基本是唯一不会重复)。
【原】Spring AOP实现对Redis的缓存同步的更多相关文章
- Spring通过AOP实现对Redis的缓存同步
废话不多说 说下思路:使用aop注解,在Service实现类添加需要用到redis的方法上,当每次请求过来则对其进行拦截,如果是查询则从redis进行get key,如果是update则删除key,防 ...
- spring aop + xmemcached 配置service层缓存策略
Memcached 作用与使用 基本介绍 1,对于缓存的存取方式,简言之,就是以键值对的形式将数据保存在内存中.在日常业务中涉及的操作无非就是增删改查.加入缓存机制后,查询的时候,对数据进行缓存,增删 ...
- Spring Boot 2整合Redis做缓存
既然是要用Redis做缓存,自然少不了安装了.但是本文主要讲Spring Boot与Redis整合.安装教程请另行百度! 1.首先是我们的Redis配置类 package com.tyc; impor ...
- 封装JedisClient.提供API实现对redis的操作
需要导包,jedis-2.8.1.jar和博主的序列化工具类SerializeUtils package com.demo.redis; import java.util.ArrayList; imp ...
- centos8平台php7.4.2安装phpredis实现对redis的访问
一,下载phpredis 1,官方下载地址: https://github.com/phpredis/phpredis/releases 2,wget下载 [root@yjweb source]# w ...
- Spring Boot从入门到精通(六)集成Redis实现缓存机制
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言 ...
- 使用方法拦截机制在不修改原逻辑基础上为 spring MVC 工程添加 Redis 缓存
首先,相关文件:链接: https://pan.baidu.com/s/1H-D2M4RfXWnKzNLmsbqiQQ 密码: 5dzk 文件说明: redis-2.4.5-win32-win64.z ...
- spring aop搭建redis缓存
SpringAOP与Redis搭建缓存 近期项目查询数据库太慢,持久层也没有开启二级缓存,现希望采用Redis作为缓存.为了不改写原来代码,在此采用AOP+Redis实现. 目前由于项目需要,只需要做 ...
- SpringBoot集成Redis实现缓存处理(Spring AOP实现)
第一章 需求分析 计划在Team的开源项目里加入Redis实现缓存处理,因为业务功能已经实现了一部分,通过写Redis工具类,然后引用,改动量较大,而且不可以实现解耦合,所以想到了Spring框架的A ...
随机推荐
- 过滤器和拦截器filter和Interceptor的区别
1.创建一个Filter过滤器只需两个步骤 创建Filter处理类 web.xml文件中配置Filter 2.Servlet中的过滤器Filter是实现了javax.servlet.Filter接口的 ...
- 关于esp32的系统初始化启动过程及设计学习方法
对于esp32,其开发程序中有且只能有一个app_main函数,该函数是用户程序的入口,这在没有调用FreeRTOS的系统中相当于函数main,但其实在app_main之前,系统还有一段初始化的过程, ...
- 使用pyinstaller打包python小程序(没有使用第三方模块)
准备: 1,xxx.py程序文件 2,自定义的图标文件:xxx.ico 图标文件应该包含常见的多分辨率格式,以便适应在不同场合显示,不能是单一图片. 你可以用专用的软件处理生成图标,不过少量的图标生产 ...
- hdu 4034
比赛时才发现自己基础算法都忘得光光了,逆向floyd i->j经过k点,只要i到j的距离大于或者等于,就把这边标记,实为去掉...此时整个图就减一条边 #include<iostream& ...
- Batch Normalization--介绍
思考 YJango的前馈神经网络--代码LV3的数据预处理中提到过:在数据预处理阶段,数据会被标准化(减掉平均值.除以标准差),以降低不同样本间的差异性,使建模变得相对简单. 我们又知道神经网络中的每 ...
- Python自动化编程-树莓派GPIO编程(二)
树莓派我们编程一般都直接用高效的python,针对于GPIO编程,python也是有这一方面的库的,这里最有名也是最常用的就是RPI.GPIO了.这个库是专门为树莓派GPIO编程所设计的,利用它你可以 ...
- Android 上传文件到 FTP 服务器
实现背景 近期接触到一个需求,就是将文件从Android系统上传到FTP服务器,虽然之前接触过FTP服务器,了解基本的使用流程,但是将此流程从使用习惯转化为代码实现还是有一定难度的.但是基本的流程还是 ...
- MySQL:索引
索引的目的在于提高查询效率,它的作用就相当于一本书的目录: 1. 常见的索引模型 1.1 哈希表 优点:适用于等值查询的场景: 缺点:范围查询效率较低: 1.2 有序数组 优点:范围查询和等值查询效率 ...
- Spark基础脚本入门实践1
1.创建数据框架 Creating DataFrames val df = spark.read.json("file:///usr/local/spark/examples/src/mai ...
- postgresql-排序
postgresql的排序. https://www.cnblogs.com/flying-tiger/p/8120046.html 1.是根据排序数据量的大小来判断,如果数据量少到能够整体放到内存的 ...