redis缓存原理与实现_基于Redis与切面实现方法通用缓存

本文介绍了如何利用自定义注解和切面实现Redis的通用方法缓存,适用于读取频繁且变更少的数据场景,以提高系统性能和并发能力。通过注解、拦截器和Redisson等技术,实现缓存命中和不命中的处理策略,同时提供测试示例说明其使用方式和效果。
摘要由CSDN通过智能技术生成

简介

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

使用缓存主要有两个用途:高性能、高并发

  • 高性能:

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

  • 高并发

    • mysql 单机支撑2K QPS就容易报警了,使用缓存的话,单机支撑的并发量轻松1s几万~十几万。

    • 缓存位于内存,内存对高并发的良好支持。

目的

  • 通过统一缓存方式,规范缓存实现。

  • 缓存命中走缓存。不命中加锁查询防止大量穿透引起雪崩;

  • 通过统一注解方式,也减少了大量的冗余代码 。

  • 减少重复,干掉重复。

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

支持场景

  • 业务自定义的查询方法均可使用;

  • 包括无参数的方法,有参数的方法;

  • 可以根据不同的参数值做缓存 ;

技术位面

  • Redis

  • Redisson

  • fastjson

  • 切面(Aspect)

  • 序列化 (Serializable)

  • SpringBoot (示例工程)

功能示例图:

9b4d746d993c3fc85020348805644fb2.png

核心实现

实现原理简介

  • 自定义注解 @CacheData ;

  • 通过切面 CacheAspect 拦截添加自定义注解的方法 ;

  • 通过参数生成Redis缓存key

    • 解析方法的参数并按照参数顺序排序

    • 将转换好的参数转换为Json字符串

      • 本来想用toString的考虑到继承等没有重写方法就改用了Json

    • 将参数进行MD5,一方面安全性,一方面保证Redis key不要过长,节约空间 ;

代码实现

1.自定义注解

/** * 自定义缓存注解 * * @author 程序员小强 * @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.拦截切面实现

/** * 统一缓存自定义注解拦截实现 * * @author 程序员小强 */@Aspect@Componentpublic 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 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;    }}
源码工程

文章中没有贴出全部的代码,只贴出了核心实现,详细源码见如下,源码中包含使用实例

源码地址:

https://github.com/mengq0815/spring-boot-example/tree/master/springBoot-api-cache

如图源码工程介绍图

aa1b737f67a7bf306c42e7e03252bdb1.png

使用方式

直接在实现的方法上使用注解即可

注解属性详见: 上文 ->  代码实现 > 自定义注解

a82b4a39153a1c8be62bfea4edece688.png

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

//自定义设置-刷新缓存CacheContext.refreshCacheFlag.set(true);//业务方法//自定义设置-刷新缓存CacheContext.refreshCacheFlag.set(false);

实例测试

测试方法接口实现

/** * 用户基本处理Service * * @author 程序员小强 * @version 1.0 */@Servicepublic class UserCacheBaseServiceImpl implements UserCacheBaseService {    private static final Logger log = LoggerFactory.getLogger(UserCacheBaseServiceImpl.class);    /**     * 测试所需的模拟数据     */    private static Map 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 ListlistAll() {        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 ListlistPageUser(Integer page, Integer pageSize) {        List 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 subUserList = userInfoList.subList(start, end);        return new ArrayList<>(subUserList);    }    /**     * 对象参数测试-分页查询用户     *     * @param queryBO     * @return     */    @Override    @CacheData(expireTime = 20 * 60)    public ListlistPageUserByObjParam(UserListQueryBO queryBO) {        int page = queryBO.getPage();        int pageSize = queryBO.getPageSize();        //模拟程序查询        List 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 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 subUserList = userInfoList.subList(start, end);        return new ArrayList<>(subUserList);    }    /**     * 普通参数+对象参数测试-分页查询用户     *     * @param page     * @param pageSize     * @param queryBO     * @return     */    @Override    @CacheData(expireTime = 20 * 50)    public ListlistPageUserByObjAndParam(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 * * @author 程序员小强 */@RestControllerpublic 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);    }}
1.返回人员列表为例

工程启动后访问localhost:8081/cache-user/listAll

第一次访问由于没有缓存,方法内部加了休眠,所以比较慢,后续访问基本上都是30毫秒左右因为缓存已经存在了

0ea30d016c03fd991d51a9b534c3987e.png

7a424ec36222457e31f723bfb4ad7bba.png

2.获取人员详情

localhost:8081/cache-user/getUserById?id=1

8d708e0ce3794c5d2ee13f0d75d5066a.png

3.分页查询人员

localhost:8081/cache-user/listPageUserByObjParam?page=1&pageSize=10

注意:缓存是根据参数生成的Key,如果参数变化,比如要查询第二页,则第一次查询是没有缓存的

cbba711ee9355848f463f47b429029eb.png

总结

  • 此方案比较适合比如xx详情等接口的缓存,分页查询这种会存在按照参数缓存,参数不一致则缓存无法命中;

  • 分页缓存推荐使用redis的List、set有序集合等组合存储。

万物有痕,小弟也不是大佬,欢迎大家指点,共同进步

其它Redis篇

基于Redis分布式限流组件设计与实例

Redis 分布式锁的java版正确实现

Redis 数据类型以及适用场景

Redis 持久化方式

Redis在Linux下的安装

Redis4.0高可用集群模式搭建

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值