Bootstrap

【场景题】架构优化 - 解耦Redis缓存与业务逻辑

1. 需求分析

某公司需要将原有的Redis缓存抽离出来,并且还要要实现:

  1. 可配置
  2. 热拔插
  3. 高可用
  4. 高通用

请问你会如何实现?

2. 思路

话不多说直接上思路:

  1. 自定义缓存注解,当容器扫描到该注解自动调用AOP想应的增强方法为原有的业务逻辑赋能
  2. 使用SpringEL表达式占位符扩展

3. 代码实现

3.1 注解创建

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRedisCatch {
    // 前缀
    String keyPrefix();
    // 匹配的value值
    String matchValue();
}

3.2 切面类

@Component
@Aspect
public class MyRedisCacheAspect {
    @Resource
    private RedisTemplate redisTemplate;
    //换成自己注解的全类名
    @Around("@annotation(com.example.redisexercises.annotation.MyRedisCatch)")
    public Object doCatch(ProceedingJoinPoint joinPoint){
        Object res = null;

        try {
            //获取当前的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();

            //获取方法上的注解
            MyRedisCatch redisCatchAnnotation = method.getAnnotation(MyRedisCatch.class);
            //获得注解中的参数
            String keyPrefix = redisCatchAnnotation.keyPrefix();
            String matchValue = redisCatchAnnotation.matchValue();

            //SpringEL解析器
            SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
            Expression expression = spelExpressionParser.parseExpression(matchValue);
            EvaluationContext context = new StandardEvaluationContext();

            //获得方法中形参
            Object[] args = joinPoint.getArgs();
            DefaultParameterNameDiscoverer defaultParameterNameDiscoverer = new DefaultParameterNameDiscoverer();
            String[] parameterNames = defaultParameterNameDiscoverer.getParameterNames(method);
            for(int i = 0;i< parameterNames.length;i++){
                context.setVariable(parameterNames[i], args[i].toString());
            }

            //拼接最终redis key
            String key = keyPrefix + ":" + expression.getValue(context).toString();

            //查询redis
            res = redisTemplate.opsForValue().get(key);
            if(res != null){
                return res;
            }
			//没查询到结果则则主业务类查数据库
            res = joinPoint.proceed();
            //最终回填到redis中,下次查询命中
            if(res != null)
                redisTemplate.opsForValue().set(key, res, 60, TimeUnit.SECONDS);
        }catch (Throwable throwable){
            throwable.printStackTrace();
        }

        return res;
    }
}

3.3 在任意方法上标注注解以实现缓存功能

@ApiOperation("获取商品")
@GetMapping("/get/{id}")
@MyRedisCatch(keyPrefix = "user", matchValue = "id")
public Order get(@PathVariable Integer id){
    //数据库查找业务逻辑
}
;