前言:刚开始采用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的缓存同步

    前言:刚开始采用spring cache作为缓存数据,到后面发现扩展性不灵活,于是基于sprig cache原理自定义一套规则用于缓存数据. 请求过程: 根据请求参数生成Key,后面我们会对生成Key ...

  2. Java Web学习系列——Maven Web项目中集成使用Spring、MyBatis实现对MySQL的数据访问

    本篇内容还是建立在上一篇Java Web学习系列——Maven Web项目中集成使用Spring基础之上,对之前的Maven Web项目进行升级改造,实现对MySQL的数据访问. 添加依赖Jar包 这 ...

  3. Spring Boot WebFlux-07——WebFlux 中 Redis 实现缓存

    第07课:WebFlux 中 Redis 实现缓存 前言 首先,补充下上一篇的内容,RedisTemplate 实现操作 Redis,但操作是同步的,不是 Reactive 的.自然,支持 React ...

  4. spring cache会默认使用redis作为缓存吗?

    web项目中,只需要配置 redis 的IP,端口,用户名和密码就可以使用redis作为缓存了,不需要在在java 代码中配置redisConfig,redisConfig只是作为缓存配置的辅助,比如 ...

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

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

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

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

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

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

  8. 今日份学习: Spring中使用AOP并实现redis缓存?

    笔记 在Spring中如何使用AOP? Spring是如何切换JDK动态代理和CGLIB的? spring.aop.proxy-target-class=true (在下方第二个链接中,原生doc中提 ...

  9. Spring Boot + Mybatis + Redis二级缓存开发指南

    Spring Boot + Mybatis + Redis二级缓存开发指南 背景 Spring-Boot因其提供了各种开箱即用的插件,使得它成为了当今最为主流的Java Web开发框架之一.Mybat ...

随机推荐

  1. 求新的集合 A=AUB(顺序表)

    #include<stdio.h> typedef int A; const int LIST_INIT_SIZE=100; const int LISTINCREMENT=10; typ ...

  2. html5 geolocation API

    清单 1. 检查浏览器支持性if (navigator.geolocation) 清单 2. 单次定位请求 API void getCurrentPosition(updateLocation, op ...

  3. DAO以及获取自动生成主键值

    package com.alibaba.sql; import java.lang.reflect.InvocationTargetException; import java.sql.Connect ...

  4. .NET Core 安装

    Visual Studio 2015 和 .NET Core 安装 安装 Visual Studio 和 .NET Core 1.安装 Visual Studio Community 2015,选择 ...

  5. C#获取桌面壁纸图片的路径(Desktop Wallpaper)

    原文 C#获取桌面壁纸图片的路径(Desktop Wallpaper) 利用 Windows 的 API 获取桌面壁纸的实际路径,使用的是 SystemParametersInfo 这个API,此AP ...

  6. Spring Boot Memory Performance

    The Performance Zone is brought to you in partnership with New Relic. Quickly learn how to use Docke ...

  7. HDU 3328 Flipper

    题解:直接建n个栈,模拟过程即可…… #include <cstdio> #include <cstring> #include <stack> using nam ...

  8. Android 4.4及以上系统下应用的状态栏颜色渐变效果的实现

    上一篇转载的博文里讲到了怎么开启状态栏透明的效果,不过如果在有ActionBar的情况下,会出现状态栏透明而ActionBar横亘在状态栏和内容之间的丑陋情况,如下图: 通过百度之后,发现了GitHu ...

  9. 算法导论 6.5.9 堆实现K路归并问题

    问题: 设计一个时间复杂度为O(NlogK)的算法,它能够将K个有序链表合并为一个有序链表,这里的N为所有输入链表包含的总的元素个数 分析: 该问题为经典的利用堆完成K路归并的问题: 当K个序列满足一 ...

  10. poj 3630 Phone List(字典树)

    题目链接: http://poj.org/problem?id=3630 思路分析: 求在字符串中是否存在某个字符串为另一字符串的前缀: 即对于某个字符串而言,其是否为某个字符串的前缀,或存在某个其先 ...