前言

面试时问到用没用过 AOP,很多回答都是用 AOP 做过日志统一处理。 给人感觉就是没做过啊

今天介绍一个用注解封装 redis 缓存的 AOP 实战

redis 缓存加速的基本逻辑

用 redis 加速数据库访问,一般会写出如下代码

@Servicepublic class UserServiceImpl implements UserService {    private final UserMapper  userMapper;    private final RedisClient redisClient;    @Autowired    public UserServiceImpl(UserMapper userMapper, RedisClient redisClient) {        this.userMapper = userMapper;        this.redisClient = redisClient;    }    @Override    public User get(Long id) {        String key = String.format("USER:%d", id);        String value = redisClient.get(key);        if (StringUtils.isNotEmpty(value)) {            return JSON.parseObject(value, User.class);        }        User user = userMapper.get(id);        if (user != null) {            redisClient.set(key, JSON.toJSONString(user));        }        return user;    }}

其实现逻辑如下

  • 根据输入参数构造 redis 的 key

  • 从 redis 里获取该 key 的值

    • 如果值不为空,则命中缓存,直接返回

  • redis 未命中

    • 穿透到数据库查询

    • 如果数据库查询到该值,缓存到 redis

    • 返回

实际上用 redis 缓存加速数据库查询基本都是这样的套路,如果在每个 service 都要这样写一遍,太繁琐了,用注解来封装一下吧

思路

通过 Spring 的 AOP 技术,拦截从数据库查询的方法,在从数据库获取结果之前,先从 redis 获取,如果 redis 命中,则直接返回;否则就继续执行从数据库获取的方法,将返回值缓存到 reids 并返回

实际上不限于从数据库获取结果,如果是从远程服务获取值,也可以采用同样的思路

实战步骤

step1:标记要拦截的方法

很显然用注解是个不错的主义,定义如下注解

/** *  这个标注用来为redis的通用化存取设定参数 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Redis {    /** 非null值 默认过期时间 **/    int DEFAULT_TTL = 4 * 60 * 60 + 10 * 60;    /** null值 默认过期时间 **/    int NULL_TTL    = 5 * 60;    /** redsi key,见 {@link RedisKeys} **/    String value();    /**     * 

* 指示方法的哪些参数用来构造key,及其顺序(编号由0开始) * * 示例 * keyArgs = {1,0,2},表示用方法的第二,第一,第三个参数,按顺序来构造key * * 默认值的意思是方法的前 n 个参数来构造key,n 最大为10 * 这样如果构造 key 的参数不多于 10 个且顺序也和方法参数一致,则可以用默认值 * */ int[] keyArgs() default { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 }; /** 执行何种操作,默认是先访问 redis **/ RedisAction action() default RedisAction.REDIS_FIRST; /** 过期时间,默认250分钟 **/ int ttl() default DEFAULT_TTL; /** 是否以同步的方式操作redis,默认是false **/ boolean sync() default false; /** 是否要缓存 null 值,默认为true **/ boolean cacheNull() default true; /** 如果要缓存null值,过期时间是多少,默认5分钟 **/ int nullTtl() default NULL_TTL;}public enum RedisAction { // 优先从 redis 获取 REDIS_FIRST, // 穿透 redis 到 db 获取 STAB_REDIS;}

这个注解用在需要拦截的方法上,还附带了一些元信息

接下来是在目标方法上使用注解

@Servicepublic class UserServiceImpl2 implements UserService {    private final UserMapper userMapper;    @Autowired    public UserServiceImpl2(UserMapper userMapper) {        this.userMapper = userMapper;    }    @Redis("USER:%d")    @Override    public User get(Long id) {        return userMapper.get(id);    }}

现在代码就很简洁明了

step2:编写拦截器

@Aspect@Componentpublic class RedisInterceptor {    private static final Logger LOG = LoggerFactory.getLogger(RedisInterceptor.class);    @Resource    private RedisClient         redisClient;    @Around("@annotation(redis)")    public Object doAround(ProceedingJoinPoint pjp, Redis redis) throws Throwable {        MethodSignature signature = (MethodSignature) pjp.getSignature();        Method method = signature.getMethod();        // 是否穿透 redis        boolean stab = redis.action() == RedisAction.STAB_REDIS;        Object[] keyArgs = getKeyArgs(pjp.getArgs(), redis.keyArgs());        String key = keyArgs == null ? redis.value() : String.format(redis.value(), keyArgs);        Class> returnType = method.getReturnType();        Object result = stab ? null : get(key, returnType);        if (result == null) {            result = pjp.proceed();            if (result != null) {                setex(key, redis.ttl(), result, redis.sync());            } else if (redis.cacheNull()) {                setex(key, redis.nullTtl(), result, redis.sync());            }        }        return result;    }    /** 获取构造 redis 的 key 的参数数组 */    private Object[] getKeyArgs(Object[] args, int[] keyArgs) {        Object[] redisKeyArgs;        int len = keyArgs.length;        if (len == 0) {            return null;        } else {            len = min(len, args.length);            redisKeyArgs = new Object[len];            int i = 0;            for (int n : keyArgs) {                redisKeyArgs[i++] = args[n];                if (i >= len) {                    break;                }            }            return redisKeyArgs;        }    }    private int min(int i, int j) {        return i > j ? j : i;    }    private void setex(final String key, final int ttl, final T data, boolean sync) {        try {            redisClient.setex(key, ttl, data, sync);        } catch (Exception e) {            LOG.error("redis set error:{}", e.getMessage(), e);        }    }    private T get(String key, Class clazz) {        try {            return redisClient.get(key, clazz);        } catch (Exception e) {            LOG.error("redis get error:{}", e.getMessage(), e);            return null;        }    }}

step3:配置拦截器

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"  xmlns:aop="http://www.springframework.org/schema/aop"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  xsi:schemaLocation="http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd     http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans.xsd     http://www.springframework.org/schema/aop      http://www.springframework.org/schema/aop/spring-aop.xsd">    <aop:aspectj-autoproxy proxy-target-class="false" />beans>

这样就 ok 了,在你想要使用 redis 缓存加速的方法上加上 redis 注解吧

关于 redis 穿透

通常情况下,都是优先从 redis 里查询结果。但也有时候需要穿透 redis,到 db 里去获取结果的。

@Redis 注解也支持这种操作,只需要设置 action 属性为 STAB_REDIS 即可

但是,同一个方法,action 要么是 STAB_REDIS,要么是 REDIS_FIRST,拦截器只能实现其中一种操作,如何才能让拦截器拦截同一个方法时,实现不同的 redis 操作呢?

有以下几个办法

  1. 方法的参数里添加一个专门的变量,用来告诉拦截器做何种操作

  2. 复制该方法为另一个方法,2个方法作用一样,注解也一样,区别是注解的 action 属性不同

  3. 两个不同名的方法,实际上代码是一样的

经过考虑,最终采用了第2种办法

总结

在 Spring 配置文件里开启注解式 AOP,使用如下配置

class="false" />

自定义注解,如下 2 个元注解必须

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)

针对自定义的注解编写拦截器代码

拦截器要用到反射,泛型啥的,一切皆有可能

拦截器配置,注意如下几个注解的使用

@Aspect@Component@Around("@annotation(******)")

redis返回的结果是null_Spring AOP 用注解封装 redis 缓存相关推荐

  1. 从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之十一 || AOP自定义筛选,Redis入门 11.1...

    大神留步 先说下一个窝心的问题,求大神帮忙,如何在Task异步编程中,使用Redis存.取Task<List<T>>泛型,有偿帮助,这里谢谢,文末有详细问题说明,可以留言或者私 ...

  2. redis-----07-----redigo基本命令操作(主要讲如何让go的struct、map展开成redis的参数,以及使用struct获取redis返回的key-value批量数组)

    1 请求回应模式 redis 与 client 之间采用请求回应模式,一个请求包对应一个回应包.但是也有例外,pub/sub 模式下,client 发送 subscribe 命令并收到回应包后,之后被 ...

  3. 搞懂分布式技术14:Spring Boot使用注解集成Redis缓存

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/a724888/article/details/80785403 为了提高性能,减少数据库的压力,使用 ...

  4. 5.概念(maven,ssm,springMvc,spring,自定义注解,二级缓存,范式,事务,mysql,线程池,map,hashmap,redis,饿汉,懒汉)

    maven是啥: 1.Maven是一个项目管理和综合工具.Maven提供了开发人员构建一个完整的生命周期框架. 创建-导入jar报–编写配置文件-实现业务功能-测试-发布上线. 2.开发团队可以自动完 ...

  5. Spring Aop 常见注解和执行顺序

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:juejin.cn/post/7062506923194581029 Spring 一开始最强大的就是 IOC / AOP 两 ...

  6. 扩展基于注解的spring缓存,使缓存有效期的设置支持方法级别-redis篇

    2019独角兽企业重金招聘Python工程师标准>>> 这里用的spring对redis的封装spring-data-redis,主要是对RedisCacheManager做一个二次 ...

  7. Redis 在CentOS 6上的 安装和部署以及redis的主从复制sentinel实现HA

    一.简介 Redis是基于内存的存储,所有数据都工作与内存中,基于键值存储(key-value store),经常拿来跟memcached做比较:memcached没有持久能力,Redis有持久能力, ...

  8. redis序列化_实例讲解Springboot以Template方式整合Redis及序列化问题

    1 简介 之前讲过如何通过Docker安装Redis,也讲了Springboot以Repository方式整合Redis,建议阅读后再看本文效果更佳: (1) Docker安装Redis并介绍漂亮的可 ...

  9. Redis笔记系列(特别总结篇)——常见配置redis.conf知识点总结

    2019独角兽企业重金招聘Python工程师标准>>> 由于前几篇说的redis配置信息量有点杂,很多是循序渐进把各个点引出的,不太方便我自己和其他小伙伴日后拿出来瞄一眼,所以,本文 ...

最新文章

  1. vmware虚拟机移植带来的问题
  2. RNN 怎么用?给初学者的小教程
  3. 深度学习入门笔记系列(三)——感知器模型和 tensorboard 的使用方法
  4. JAVA15.JDK15新特性.4 TextBlock
  5. PAT天梯赛L3-004 肿瘤诊断
  6. ant vue 语言_Ant Design Vue是什么
  7. ARM MMU工作原理剖析[转]
  8. 前端学习(2705):重读vue电商网站26之路由导航守卫控制访问权限
  9. ajax实现局部删除,Express+AdminLTE+hbs+Ajax实现局部刷新终极版(第二部分)
  10. Socket相关操作超时
  11. android开源2016_2016年开源年鉴:现在提供印刷版
  12. java程序设计_Java程序设计--接口interface(笔记)
  13. WMS仓储管理系统实施时要注意哪些事项?
  14. [fastjson] - fastjson中 JSONObject 和 JSONArray
  15. 之前8年都在上班工资16000,厌倦了天天上班的日子,就裸辞了。现在很迷茫,下一步怎么办?
  16. 12.1-12.5 LNMP架构介绍 , MySQL安装 , PHP安装, Nginx介绍
  17. 基于python下django框架 实现闲置物品二手跳蚤市场交易系统详细设计
  18. 吴恩达---机器学习的流程(持续更新)
  19. xml 中的 大于号,小于号
  20. 从I到R:人工智能语言简史

热门文章

  1. Android开源库集合(UI效果)
  2. GitLab 8.9 新增文件锁 和 U2F硬件支持
  3. android 网络gif_Android SurfaceView实现GIF动画架包,播放GIF动画
  4. qt qstandarditemmodel rowcount获取行数不正确_MIL+QT实践教程十
  5. 如何新建Spring Boot工程
  6. ip登陆异常 php,PHP实例:PHP制作登录异常ip检测功能的实例代码
  7. zookeeper的acl权限控制_zookeeper权限acl与四字命令
  8. python输入文字字符串、如何提取字符_如何使用python从字符串中提取url?
  9. python采用函数式编程模式-浅谈Python 函数式编程
  10. labimage 怎样旋转图片_隔断墙见多了,头次见能180旋转任意移动,还多出一面墙来储物...