前言:刚开始采用spring cache作为缓存数据,到后面发现扩展性不灵活,于是基于sprig cache原理自定义一套规则用于缓存数据。


请求过程:

  1. 根据请求参数生成Key,后面我们会对生成Key的规则,进一步说明;
  2. 根据Key去缓存服务器中取数据,如果取到数据,则返回数据,如果没有取到数据,则执行service中的方法调用dao从DB中获取数据,同时成功后将数据放到缓存中。
  3. 删除、新增、修改会触发更新缓存的拦截类对缓存服务器进行更新。

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的缓存同步的更多相关文章

  1. Spring通过AOP实现对Redis的缓存同步

    废话不多说 说下思路:使用aop注解,在Service实现类添加需要用到redis的方法上,当每次请求过来则对其进行拦截,如果是查询则从redis进行get key,如果是update则删除key,防 ...

  2. spring aop + xmemcached 配置service层缓存策略

    Memcached 作用与使用 基本介绍 1,对于缓存的存取方式,简言之,就是以键值对的形式将数据保存在内存中.在日常业务中涉及的操作无非就是增删改查.加入缓存机制后,查询的时候,对数据进行缓存,增删 ...

  3. Spring Boot 2整合Redis做缓存

    既然是要用Redis做缓存,自然少不了安装了.但是本文主要讲Spring Boot与Redis整合.安装教程请另行百度! 1.首先是我们的Redis配置类 package com.tyc; impor ...

  4. 封装JedisClient.提供API实现对redis的操作

    需要导包,jedis-2.8.1.jar和博主的序列化工具类SerializeUtils package com.demo.redis; import java.util.ArrayList; imp ...

  5. centos8平台php7.4.2安装phpredis实现对redis的访问

    一,下载phpredis 1,官方下载地址: https://github.com/phpredis/phpredis/releases 2,wget下载 [root@yjweb source]# w ...

  6. Spring Boot从入门到精通(六)集成Redis实现缓存机制

    Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持久化的日志型.Key-Value数据库,并提供多种语言 ...

  7. 使用方法拦截机制在不修改原逻辑基础上为 spring MVC 工程添加 Redis 缓存

    首先,相关文件:链接: https://pan.baidu.com/s/1H-D2M4RfXWnKzNLmsbqiQQ 密码: 5dzk 文件说明: redis-2.4.5-win32-win64.z ...

  8. spring aop搭建redis缓存

    SpringAOP与Redis搭建缓存 近期项目查询数据库太慢,持久层也没有开启二级缓存,现希望采用Redis作为缓存.为了不改写原来代码,在此采用AOP+Redis实现. 目前由于项目需要,只需要做 ...

  9. SpringBoot集成Redis实现缓存处理(Spring AOP实现)

    第一章 需求分析 计划在Team的开源项目里加入Redis实现缓存处理,因为业务功能已经实现了一部分,通过写Redis工具类,然后引用,改动量较大,而且不可以实现解耦合,所以想到了Spring框架的A ...

随机推荐

  1. Chapter5 生长因子、受体和癌症

    一.Src蛋白是一种蛋白激酶 可以磷酸化不同的底物,调节不同的通路 Src激酶主要磷酸化酪氨酸残基,而别的激酶主要磷酸化色氨酸.苏氨酸残基 二.EGF受体拥有酪氨酸激酶功能 胞内结构域有Src蛋白的同 ...

  2. lgwr的两种模式(post/wait和polling)

    11.2之前,oracle的lgwr写入模式为post/wait 11.2之后新增了polling模式,可以与post/wait模式自动切换 通过隐藏参数 _use_adaptive_log_file ...

  3. jenkins+donet core持续集成环境搭建

    一.Jenins+GitHub 参考 另外需要配置Global Tool Configuration 如果没有安装git,需下载安装,下载地址 二.jenkins发布donet core应用 1.配置 ...

  4. 下单快发货慢:一个 JOIN SQL 引起 SqlClient 读取数据慢的奇特问题

    最近遇到一个非常奇特的问题,在一个 ASP.NET Core 项目中从 SQL Server 2008 R2 中查询获取 100 条记录竟然耗时 10 多秒,如果是查询本身慢,那到不是什么奇特的问题. ...

  5. LeetCode算法题(长期更新)

    1.给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标. 你可以假设每种输入只会对应一个答案.但是,你不能重复利用这个数组中同样 ...

  6. 精通Linux

    1, linux 启动流程,详细 2,grub , grub2 3, 文件系统,不同文件系统的特性 ext3 , ext 4 ,xfs 4, 不同目录的作用, 分区 5,用户管理 6,文件权限,目录挂 ...

  7. MySQL 字符集utf8和utf-8的关系

    目录 什么是字符集(character set) 校对规则(collation) ASCII码 Unicode国际化支持 UTF-8 utf8 utf8与utf8mb4的关系 超集 字符集设置 什么是 ...

  8. js设计模式小结

    1 构造函数模式 var Person = function(name){ this.name = name; this.getName = function(){ console.log(this. ...

  9. [EXP]Memu Play 6.0.7 - Privilege Escalation

    # Exploit Title: Memu Play - Privilege Escalation (PoC) # Date: // # Author: Alejandra Sánchez # Ven ...

  10. 测试工具之RobotFramework关键字和快捷键

    RF中关键字很多,即使经常使用也有些关键字没有使用过,所以我们就需要记住一些常用的关键字,在使用中本人整理了部分关键字.快捷键和部分RF的常识 1.F5 如果只记得关键字部分,可以通过F5呼出关键字查 ...