从零开始SpringCloud Alibaba实战(55)——基于自定义注解+切面+Redis通用接口缓存实现

简介

随着系统QPS逐渐提高,在一些读取频繁变更较少数据场景下,适当添加缓存不仅能提升用户访问速度还可以减轻系统压力。
使用缓存主要有两个用途:高性能、高并发

高性能:

查询mysql耗时需要300ms,存到缓存redis,每次查询仅需要几毫秒,性能瞬间提升百倍。

高并发

mysql 单机支撑2K QPS就容易报警了,使用缓存的话,单机支撑的并发量轻松1s几万~十几万。
缓存位于内存,内存对高并发的良好支持。

目的

通过统一缓存方式,规范缓存实现。
缓存命中走缓存。不命中加锁查询防止大量穿透引起雪崩;
通过统一注解方式,也减少了大量的冗余代码 。
减少重复,干掉重复。

本文主要讲解,一款基于自定义注解拦截+切面+redis的通用方法缓存方式 ;

支持场景

业务自定义的查询方法均可使用;
包括无参数的方法,有参数的方法;
可以根据不同的参数值做缓存 ;

技术位面

Redis
Redisson
fastjson
切面(Aspect)
序列化 (Serializable)
SpringBoot (示例工程)

核心实现

实现原理简介

自定义注解 @CacheData ;
通过切面 CacheAspect 拦截添加自定义注解的方法 ;
通过参数生成Redis缓存key
解析方法的参数并按照参数顺序排序
将转换好的参数转换为Json字符串
本来想用toString的考虑到继承等没有重写方法就改用了Json
将参数进行MD5,一方面安全性,一方面保证Redis key不要过长,节约空间 ;

代码实现

1.自定义注解

/**
 * 自定义缓存注解
 *
 *  @see CacheAspect
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheData {

    /**
     * 自定义缓存key前缀
     * 默认:方法名
     */
    String keyPrefix() default "";

    /**
     * 缓存过期时间
     * 单位默认:秒,不设置默认10分钟
     */
    int expireTime() default 600;

    /**
     * 缓存过期-随机数
     * 单位默认:秒,默认0分钟
     * 注:设置后实际过期时间,
     * 会在expireTime基础上继续累积(0~randomExpire)之间的秒数,防止缓存大量失效大面积穿透,造成雪崩
     */
    int randomExpire() default 0;

    /**
     * 是否存储为null 的返回
     * 注:防止缓存穿透,默认true,建议查询为空时,也进行缓存
     */
    boolean storageNullFlag() default true;
}

2.拦截切面实现

/**
 * 统一缓存自定义注解拦截实现

 */
@Aspect
@Component
public class CacheAspect {
    private static final Logger log = LoggerFactory.getLogger(CacheAspect.class);
    private static final String EMPTY = "";
    private static final String POINT = ".";
    private static final String CACHE_KEY_PREFIX = "cache.aspect:";
    private static final String LOCK_KEY_PREFIX = "lock.";

    @Resource
    private RedisCache redisCache;

    /**
     * redisson client对象
     */
    @Resource
    private RedissonClient redisson;

    /**
     * 匹配所有使用以下注解的方法
     * 注意:缓存是基于类+方法+参数内容做的缓存key,重载方法可能会出现问题
     * 禁止在同一个类,方法名相同的两个方法使用
     *
     * @see CacheData
     */
    @Pointcut("@annotation(com.example.cache.annotation.CacheData)")
    public void pointCut() {
    }

    /**
     * 拦截添加缓存注解的方法
     *
     * @param pjpParam
     * @return
     * @throws Throwable
     * @see CacheData
     */
    @Around("pointCut()&&@annotation(cacheData)")
    public Object logAround(ProceedingJoinPoint pjpParam, CacheData cacheData) throws Throwable {
        //注解为空
        if (null == cacheData) {
            return pjpParam.proceed(pjpParam.getArgs());
        }
        //仅用于方法
        if (null == pjpParam.getSignature() || !(pjpParam.getSignature() instanceof MethodSignature)) {
            return pjpParam.proceed(pjpParam.getArgs());
        }
        //方法实例
        Method method = ((MethodSignature) pjpParam.getSignature()).getMethod();
        //方法类名
        String className = pjpParam.getSignature().getDeclaringTypeName();
        //方法名
        String methodName = method.getName();

        log.debug("[ CacheAspect ] >> notReadCacheFlag:{} , refreshCacheFlag :{} , method:{} ",
                CacheContext.notReadCacheFlag.get(), CacheContext.refreshCacheFlag.get(), className + methodName);
        //已设置-不需要读取缓存
        if (null != CacheContext.notReadCacheFlag.get() && CacheContext.notReadCacheFlag.get()) {
            return pjpParam.proceed(pjpParam.getArgs());
        }

        //生成缓存key
        String cacheKey = this.getCacheKey(cacheData, className, methodName, pjpParam);
        //已设置-需要刷新缓存
        if (null != CacheContext.refreshCacheFlag.get() && CacheContext.refreshCacheFlag.get()) {
            Object resultValue = pjpParam.proceed(pjpParam.getArgs());
            //刷新缓存
            redisCache.putByte(cacheKey, SerializableUtil.serialize(resultValue, cacheKey), this.getExpireTime(cacheData));
            return resultValue;
        }

        Object value = null;
        //缓存中读取值
        byte[] cacheByteValue = redisCache.getByte(cacheKey);
        if (null != cacheByteValue && cacheByteValue.length > 0) {
            //反序列化对象
            value = SerializableUtil.deserialize(cacheByteValue);
        }
        //缓存中存在并且不为空-直接返回
        if (null != value && !cacheKey.equals(value.toString())) {
            log.info("[ CacheAspect ] >> [ get from cache in first ] method:{} , cacheKey:{}", className + methodName, cacheKey);
            return value;
        }
        //如果设置了允许存储null值,若缓存key存在,并且value与自定义key相同 > 则直接返回 null
        if (cacheData.storageNullFlag() && redisCache.exists(cacheKey)) {
            log.info("[ CacheAspect ] >> [ get from cache in first value is null ] method:{} , cacheKey:{}", className + methodName, cacheKey);
            return null;
        }

        //若缓存中不存在 > 则执行方法,并重入缓存
        //加锁防止大量穿透
        RLock lock = redisson.getLock(LOCK_KEY_PREFIX + cacheKey);
        lock.lock();
        try {
            //二次尝试从缓存中读取
            byte[] cacheByteValueSecond = redisCache.getByte(cacheKey);
            if (null != cacheByteValueSecond && cacheByteValueSecond.length > 0) {
                //反序列化对象
                value = SerializableUtil.deserialize(cacheByteValueSecond);
            }
            //缓存中存在并且不为空-直接返回
            if (null != value && !cacheKey.equals(value.toString())) {
                log.info("[ CacheAspect ] >> [ get from cache in second ] method:{} , cacheKey:{}", className + methodName, cacheKey);
                return value;
            }
            //如果设置了允许存储null值,若缓存key存在,并且value与自定义key相同 > 则直接返回 null
            if (cacheData.storageNullFlag() && redisCache.exists(cacheKey)) {
                log.info("[ CacheAspect ] >> [ get from cache in second value is null ] method:{} , cacheKey:{}", className + methodName, cacheKey);
                return null;
            }

            //执行方法-并获得返回值
            value = pjpParam.proceed(pjpParam.getArgs());

            //返回值不为空-存入缓存并返回
            if (null != value) {
                //存入缓存
                redisCache.putByte(cacheKey, SerializableUtil.serialize(value, cacheKey), this.getExpireTime(cacheData));
                return value;
            }

            //返回值为空-是否设置需要存储空对象
            if (cacheData.storageNullFlag()) {
                //存入缓存,空返回值时value也存储key
                redisCache.putByte(cacheKey, SerializableUtil.serialize(cacheKey), this.getExpireTime(cacheData));
                return null;
            }
            return null;
        } finally {
            //解锁
            lock.unlock();
            log.info("[ CacheAspect ] >> un lock method:{} , cacheKey:{}", className + methodName, cacheKey);
        }
    }

    /**
     * 生成缓存key
     *
     * @param cacheData
     * @param className
     * @param methodName
     * @param pjpParam
     * @return
     */
    private String getCacheKey(CacheData cacheData, String className, String methodName, ProceedingJoinPoint pjpParam) {
        //缓存key前缀
        String keyPrefix = cacheData.keyPrefix();
        if (EMPTY.equals(keyPrefix)) {
            keyPrefix = methodName;
        }
        //方法全路径(类名+方法名)
        String methodPath = className + POINT + methodName;
        //若方法参数为空
        if (pjpParam.getArgs() == null || pjpParam.getArgs().length == 0) {
            return CACHE_KEY_PREFIX + keyPrefix + POINT + DigestUtils.md5Hex(methodPath);
        }
        //参数序号
        int i = 0;
        //按照参数顺序,拼接方法参数
        Map<String, Object> paramMap = new LinkedHashMap<>(pjpParam.getArgs().length);
        for (Object obj : pjpParam.getArgs()) {
            i++;
            if (obj != null) {
                paramMap.put(obj.getClass().getName() + i, obj);
            } else {
                paramMap.put("NULL" + i, "NULL");
            }
        }
        String paramJson = JSON.toJSONString(paramMap);
        log.debug("[ CacheAspect ] >> param-JSON:{}", JSON.toJSONString(paramMap));
        return CACHE_KEY_PREFIX + keyPrefix + POINT + DigestUtils.md5Hex(paramJson);
    }

    /**
     * 计算过期时间 如果缓存设置了需要延迟失效,
     * 取设置的延迟时间1-2倍之间的一个随机值作为真正的延迟时间值
     */
    private int getExpireTime(CacheData cacheData) {
        int expire = cacheData.expireTime();

        //设置为0,则默认效期到当天的截止
        if (expire == 0) {
            expire = (int) (60 * 60 * 24 - ((System.currentTimeMillis() / 1000 + 8 * 3600) % (60 * 60 * 24)));
        }

        int randomExpire = 0;
        //若设置>0 , 失效时间加上(0~设置)的值内的随机数
        if (cacheData.randomExpire() > 0) {
            randomExpire = new Random().nextInt(cacheData.randomExpire());
        }

        return expire + randomExpire;
    }
}

使用方式

直接在实现的方法上使用注解即可
注解属性详见: 上文 -> 代码实现 > 自定义注解

如果调用方法的时候想让不走缓存可以使用-强制包裹内的方法不走缓存

//自定义设置-刷新缓存
CacheContext.refreshCacheFlag.set(true);

//业务方法

//自定义设置-刷新缓存
CacheContext.refreshCacheFlag.set(false);

实例测试

测试方法接口实现

/**
 * 用户基本处理Service
 *

 * @version 1.0
 */
@Service
public class UserCacheBaseServiceImpl implements UserCacheBaseService {
    private static final Logger log = LoggerFactory.getLogger(UserCacheBaseServiceImpl.class);

    /**
     * 测试所需的模拟数据
     */
    private static Map<Integer, UserInfo> MOCK_USER_MAP = new HashMap<>();

    static {
        MOCK_USER_MAP.put(1, new UserInfo(1, "zhangsan", "张三", 1));
        MOCK_USER_MAP.put(2, new UserInfo(2, "lisi", "李四", 1));
        MOCK_USER_MAP.put(3, new UserInfo(3, "wangwu", "王五", 2));
        MOCK_USER_MAP.put(4, new UserInfo(4, "xiaoliu", "小六", 1));
        MOCK_USER_MAP.put(5, new UserInfo(5, "xiaoqi", "小七", 2));
        MOCK_USER_MAP.put(6, new UserInfo(6, "xiaoba", "小八", 0));
        MOCK_USER_MAP.put(7, new UserInfo(7, "nihao", "你好", 3));
        MOCK_USER_MAP.put(8, new UserInfo(8, "wohao", "我好", 1));
        MOCK_USER_MAP.put(9, new UserInfo(9, "tahao", "他好", 2));
        MOCK_USER_MAP.put(10, new UserInfo(10, "dajiahao", "大家好", 2));
        MOCK_USER_MAP.put(11, new UserInfo(11, "womenhao", "我们好", 1));
        MOCK_USER_MAP.put(12, new UserInfo(12, "feichanghao", "非常好", 3));
        MOCK_USER_MAP.put(13, new UserInfo(13, "xiaoxiao", "小小", 2));
        MOCK_USER_MAP.put(14, new UserInfo(14, "dada", "大大", 0));
        MOCK_USER_MAP.put(15, new UserInfo(15, "haha", "哈哈", 1));
    }

    /**
     * 无参数方法测试
     *
     * @return
     */
    @Override
    @CacheData(keyPrefix = "userListAll")
    public List<UserInfo> listAll() {
        log.info("UserCacheBaseServiceImpl >> listAll");

        //睡眠-模拟查询时间
        this.sleep(500);
        return new ArrayList<>(MOCK_USER_MAP.values());
    }

    /**
     * 根据用户ID查询
     *
     * @param id
     */
    @Override
    @CacheData(expireTime = 20 * 60)
    public UserInfo getUserById(Integer id) {
        log.info("UserCacheBaseServiceImpl >> getUserById id:{}", id);
        if (null == id) {
            return null;
        }
        //睡眠-模拟查询时间
        this.sleep(1000);
        return MOCK_USER_MAP.get(id);
    }

    /**
     * 根据用户ID和状态查询
     *
     * @param id
     * @param status
     * @return
     */
    @Override
    @CacheData(expireTime = 25 * 60, storageNullFlag = false)
    public UserInfo getUserByIdAndStatus(Integer id, Integer status) {
        log.info("UserCacheBaseServiceImpl >> getUserByIdAndStatus id:{},status:{}", id, status);
        if (null == id) {
            return null;
        }
        //睡眠-模拟查询时间
        this.sleep(1000);
        UserInfo userInfo = MOCK_USER_MAP.get(id);
        if (userInfo.getStatus().equals(status)) {
            return userInfo;
        }
        return null;
    }

    /**
     * 根据用户ID查询-可存储null值
     *
     * @param id
     * @return
     */
    @Override
    @CacheData
    public UserInfo getUserByIdStorageNull(Integer id) {
        return this.getUserById(id);
    }

    /**
     * 普通参数测试-分页查询用户
     *
     * @param page
     * @param pageSize
     * @return
     */
    @Override
    @CacheData(expireTime = 20 * 60)
    public List<UserInfo> listPageUser(Integer page, Integer pageSize) {
        List<UserInfo> userInfoList = new ArrayList<>(MOCK_USER_MAP.values());
        //计算偏移
        int start = pageSize * (page - 1);
        int end = start + pageSize;
        if (end > userInfoList.size()) {
            end = userInfoList.size();
        }
        log.info("UserCacheBaseServiceImpl >> listPageUser page:{},pageSize:{},start:{},end:{}", page, +pageSize, start, +end);

        //睡眠-模拟查询时间
        this.sleep(2000);
        List<UserInfo> subUserList = userInfoList.subList(start, end);

        return new ArrayList<>(subUserList);
    }

    /**
     * 对象参数测试-分页查询用户
     *
     * @param queryBO
     * @return
     */
    @Override
    @CacheData(expireTime = 20 * 60)
    public List<UserInfo> listPageUserByObjParam(UserListQueryBO queryBO) {
        int page = queryBO.getPage();
        int pageSize = queryBO.getPageSize();

        //模拟程序查询
        List<UserInfo> userInfoList = new ArrayList<>(MOCK_USER_MAP.size());
        UserInfo mapValue = null;
        boolean canReturnFlag = true;
        boolean userNameNotNullFlag = (null != queryBO.getUserName() && !"".equals(queryBO.getUserName()));
        boolean userStatusNotNullFlag = (null != queryBO.getStatus());
        for (Map.Entry<Integer, UserInfo> map : MOCK_USER_MAP.entrySet()) {
            mapValue = map.getValue();
            //检索条件都不为空
            if (userNameNotNullFlag && userStatusNotNullFlag) {
                canReturnFlag = mapValue.getUserName().contains(queryBO.getUserName()) && mapValue.getStatus().equals(queryBO.getStatus());
            } else if (userNameNotNullFlag) {
                //用户名称检索
                canReturnFlag = mapValue.getUserName().contains(queryBO.getUserName());
            } else if (userStatusNotNullFlag) {
                //用户状态检索
                canReturnFlag = mapValue.getStatus().equals(queryBO.getStatus());
            }
            if (!canReturnFlag) {
                continue;
            }
            userInfoList.add(mapValue);
        }
        //计算偏移
        int start = pageSize * (page - 1);
        int end = start + pageSize;
        if (end > userInfoList.size()) {
            end = userInfoList.size();
        }
        if (start > end) {
            start = end;
        }
        log.info("UserCacheBaseServiceImpl >> listPageUserByCondition page:" + page + " pageSize:" + pageSize + " start:" + start + " end:" + end);

        //睡眠-模拟查询时间
        this.sleep(2000);
        List<UserInfo> subUserList = userInfoList.subList(start, end);

        return new ArrayList<>(subUserList);
    }

    /**
     * 普通参数+对象参数测试-分页查询用户
     *
     * @param page
     * @param pageSize
     * @param queryBO
     * @return
     */
    @Override
    @CacheData(expireTime = 20 * 50)
    public List<UserInfo> listPageUserByObjAndParam(Integer page, Integer pageSize, UserListQueryBO2 queryBO) {
        UserListQueryBO listQueryBo = new UserListQueryBO();
        listQueryBo.setPage(page);
        listQueryBo.setPageSize(pageSize);
        listQueryBo.setUserName(queryBO.getUserName());
        listQueryBo.setStatus(queryBO.getStatus());
        return this.listPageUserByObjParam(listQueryBo);
    }

    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (Exception e) {
            log.info("UserCacheBaseServiceImpl >> sleep  exception:", e);
        }
    }
}

测试Controller

/**
 * 测试 controller

 */
@RestController
public class CacheTestController {

    @Autowired
    private UserCacheBaseService userCacheBaseService;

    /**
     * 无参数方法测试-查询全部用户
     */
    @ResponseBody
    @RequestMapping("/listAll")
    public Response listAll() {
        return Response.success(userCacheBaseService.listAll());
    }

    /**
     * 根据用户ID查询
     *
     * @param id
     */
    @ResponseBody
    @RequestMapping("/getUserById")
    public Response getUserById(Integer id) {
        return Response.success(userCacheBaseService.getUserById(id));
    }

    /**
     * 根据用户ID查询
     *
     * @param id
     */
    @ResponseBody
    @RequestMapping("/getUserByIdAndStatus")
    public Response getUserByIdAndStatus(@RequestParam("id") Integer id, @RequestParam("status") Integer status) {
        return Response.success(userCacheBaseService.getUserByIdAndStatus(id, status));
    }

    /**
     * 根据用户ID查询-可存储null值
     *
     * @param id
     */
    @ResponseBody
    @RequestMapping("/getUserByIdStorageNull")
    public Response getUserByIdStorageNull(Integer id) {
        return Response.success(userCacheBaseService.getUserByIdStorageNull(id));
    }

    /**
     * 普通参数测试-分页查询用户
     *
     * @param page
     * @param pageSize
     */
    @ResponseBody
    @RequestMapping("/listPageUser")
    public Response listPageUser(Integer page, Integer pageSize) {
        return Response.success(userCacheBaseService.listPageUser(page, pageSize));
    }

    /**
     * 对象参数测试-分页查询用户
     *
     * @param queryBO
     */
    @ResponseBody
    @RequestMapping("/listPageUserByObjParam")
    public Response listPageUserByObjParam(UserListQueryBO queryBO) {
        return Response.success(userCacheBaseService.listPageUserByObjParam(queryBO));
    }

    /**
     * 普通参数+对象参数测试-分页查询用户
     *
     * @param page
     * @param pageSize
     * @param queryBO
     */
    @ResponseBody
    @RequestMapping("/listPageUserByObjAndParam")
    public Response listPageUserByObjAndParam(Integer page, Integer pageSize, UserListQueryBO2 queryBO) {
        return Response.success(userCacheBaseService.listPageUserByObjAndParam(page, pageSize, queryBO));
    }

    /**
     * 不使用缓存直接执行方法
     *
     * @param id
     */
    @ResponseBody
    @RequestMapping("/getUserById-notReadCacheFlag")
    public Response getUserByIdNotReadCache(Integer id) {
        //自定义设置不走缓存
        CacheContext.notReadCacheFlag.set(true);
        UserInfo userInfo = userCacheBaseService.getUserById(id);
        CacheContext.notReadCacheFlag.set(false);
        return Response.success(userInfo);
    }

    /**
     * 不使用缓存-查询并更新缓存
     *
     * @param id
     */
    @ResponseBody
    @RequestMapping("/getUserById-refreshCache")
    public Response getUserByIdRefreshCache(Integer id) {
        //自定义设置-刷新缓存
        CacheContext.refreshCacheFlag.set(true);
        UserInfo userInfo = userCacheBaseService.getUserById(id);
        CacheContext.refreshCacheFlag.set(false);
        return Response.success(userInfo);
    }
}

总结

此方案比较适合比如xx详情等接口的缓存,分页查询这种会存在按照参数缓存,参数不一致则缓存无法命中;
分页缓存推荐使用redis的List、set有序集合等组合存储。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值