Spring AOP:用注解访问 redis

内容纲要

前言

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

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

redis 缓存加速的基本逻辑

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

@Service
public 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的<b>通用化</b>存取设定参数
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Redis {

    /** 非null值 默认过期时间 **/
    int DEFAULT_TTL = 4 * 60 * 60 + 10 * 60;

    /** null值 默认过期时间 **/
    int NULL_TTL    = 5 * 60;

    /** redsi key,见 {@link RedisKeys} **/
    String value();

    /**
     * <pre>
     *  指示方法的哪些参数用来构造key,及其顺序(编号由0开始)
     *  
     *  示例
     *      keyArgs = {1,0,2},表示用方法的第二,第一,第三个参数,按顺序来构造key
     *  
     *  默认值的意思是方法的前 n 个参数来构造key,n 最大为10
     *  这样如果构造 key 的参数不多于 10 个且顺序也和方法参数一致,则可以用默认值
     * </pre>
     */
    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;

}

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

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

@Service
public 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
@Component
public 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 <T> 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> T get(String key, Class<T> 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 -->
    <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 操作呢?

有以下几个办法

  • 方法的参数里添加一个专门的变量,用来告诉拦截器做何种操作
  • 复制该方法为另一个方法,2个方法作用一样,注解也一样,区别是注解的 action 属性不同
  • 通过方法名来区分,比如方法名里包含 stab 表示要穿透到 db

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

总结

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

<aop:aspectj-autoproxy proxy-target-class="false" />

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

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

针对自定义的注解编写拦截器代码:拦截器要用到反射,泛型啥的,一切皆有可能

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

@Aspect
@Component
@Around("@annotation(******)")
Spring AOP:用注解访问 redis

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Scroll to top
粤ICP备2020114259号 粤公网安备44030402004258