• 【原】Spring AOP实现对Redis的缓存同步


        前言:刚开始采用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;
        }
    View Code

        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(这样基本是唯一不会重复)。

  • 相关阅读:
    android.graphics(2)
    Android 性能优化:字体 (为自定义字体提供字体内存缓存)
    管道相关函数(1)-pipe
    【译】用boosting构建简单的目标分类器
    Ubuntu下matlab快捷键设置
    mysql导入sql文件,乱码,一个例子
    PHOG特征
    图像卷积、相关以及在MATLAB中的操作
    matlab实现hog特征
    操蛋的CTex
  • 原文地址:https://www.cnblogs.com/zdd-java/p/5950064.html
Copyright © 2020-2023  润新知