CaffeineCache+Redis 接入系统做二层缓存思路实现(借鉴 mybatis 二级缓存、自动装配源码)

news2024/11/25 1:52:09

本文目录

    • 前言
    • 本文术语
    • 本文项目地址
    • 设计思路
    • 开发思路
    • @DoubleCacheAble 双缓存注解(如何设计?)
    • 动态条件表达式?例如:#a.id=?(如何解析?)
    • 缓存切面(如何设计?)
    • 缓存 CRUD 如何设计?(使用委派模式)
    • 整合自动装配
    • Redis 过期 Key 如何处理?
    • 查缓存测试
    • 过期 Key 清除测试
    • 推荐阅读

前言

现在手上有个系统写操作比较少,很多接口都是读操作,也就是写多读少,性能上遇到瓶颈了,正所谓前人栽树、后人乘凉,原先系统每次都是查数据库的,性能比较低,如果先查 redis,redis 没数据再查数据库的话,但是还可以更快,那就是使用内存查询,依次按照内存、redis、db的顺序从快到慢查询,可使系统整体的性能提升一个档次,但是仅限于读多写少的场景,写多读少的场景没必要搞这么多缓存,搞多了缓存一致性也是个问题,就好比 mysql 数据库的读多写少,我们可以用 MYISM 存储引擎。

本文术语

  • CaffeineCache:一级缓存
  • Redis:二级缓存

本文项目地址

此项目已收录于 Gitee,感兴趣的小伙伴可以克隆下来去查看一下,也欢迎提出宝贵意见大家一起来优化这个项目。
MRCache:https://gitcode.net/qq_42875345/mrcache

设计思路

给系统加二层缓存,怎么加?每个接口都加个判断,先从内存查,内存没数据再查 redis 再查 db ?那工作量太大了,且代码耦合性太高,代码看着也难看一大坨同质化的代码。先说个结论。如果你们的项目架构比较好,所有本地接口或者是 Rpc 接口调用,采用了责任链来实现只需在责任链头部新增一个,查缓存的节点即可。责任链设计模式精讲入口,
如果没用到责任链,那利用 Aop 切面+自定义注解+ Spel 框架+ CaffeineCache 内存框架 来实现即可,工作量也不大,加个切面即可。接下来进入实战。

开发思路

下面贴一段 Spring 缓存中的 @CacheAble 注解使用代码,我们配个 RedisCacheManager 后,使用此注解即可将返回结果存入Redis。Redis 中有缓存则不会执行方法中的逻辑。思考那么是否我们可以写一个 @DoubleCacheAble 注解,将原先查 Redis 的逻辑替换成,先查本地缓存、再查 Redis、最后查 db 的逻辑呢?答案是可以的且有俩种实现方式。

@Cacheable(value = "doubleCache: ", key = "#student.sId", unless = "0")
public Object testCacheable(Student student) throws InterruptedException {
    Thread.sleep(1000 * 10);
    return map;
}
  1. 方式一:重写 Cache 、CacheManager 、CacheResolver 、KeyGenerator 接口,然后定制化里面的方法,改成自己的逻辑即可,加多少层缓存都没问题。二开比较繁琐,且容易出错

举个例子就拿 @CacheAble 的使用来说,为什么每次我们使用这些注解前都要加如下的配置,那是因为 spring.data.redis 包帮我们二次封装了 Cache、CacheManager 的逻辑,且提供了默认的 KeyGenerator、CacheResolver 等实现类,感兴趣的小伙子可以自行 debug 源码

@Data
@ConfigurationProperties(prefix = "spring.redis")
@EnableCaching
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
    private int database;
    private String host;
    private int port;
    private String password;

    @Bean
    public CacheManager cacheManager() {
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ZERO)
                .disableCachingNullValues()
                .computePrefixWith(cacheName -> "caching_fm:" + cacheName);
        RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory())
                .cacheDefaults(configuration) // 默认配置(强烈建议配置上)。  比如动态创建出来的都会走此默认配置
                .build();
        return redisCacheManager;
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
        configuration.setHostName(host);
        configuration.setPassword(RedisPassword.of(password));
        configuration.setPort(port);
        configuration.setDatabase(database);
        LettuceConnectionFactory factory = new LettuceConnectionFactory(configuration);
        return factory;
    }


}

在这里插入图片描述

还有一种方式也是最容易实现的一种方式就是前言提到的加一层切面,对所有查询操作切入,织入查二层缓存的逻辑。

@DoubleCacheAble 双缓存注解(如何设计?)

高效简洁的开发当然少不了我们的自定义注解辣,完全对标 @CacheAble ,支持动态 SPEL 解析,是否缓存空值等等。日后需要增加更复杂的功能完善该注解就行。一个注解代码不做过多解释。

//作用于方法
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCacheAble {
    //缓存key:静态写死部分
    String value();

    //缓存key:动态spel部分
    String key();

    //操作类型
    String type();

    //是否缓存空值,默认不缓存空
    String unLess() default "0";
}

动态条件表达式?例如:#a.id=?(如何解析?)

一开始我还天真的以为,要不要写个算法来实现,想想都头大。后来想着 @CacheAble 这个注解不是已经实现了这个功能吗,把他里面的源码 copy 出来不就行了。但是 copy 了一会发现不对劲,各种缺包,于是乎开始 debug 源码,直到 debug 到如下这行代码,#student.id 被解析了,然后发现了 Spring 里面的存在一种名叫 SPEL 解析的技术包,拿来即用。

在这里插入图片描述
然后摸索了一会便有了我如下的这个 demo ,就是对源码的封装解析了一下。本质都是利用 Spring 里面的工具类。可以看到动态表达式已经被我成功解析,不得感叹我可真是个小天才。

public static void main(String[] args) throws NoSuchMethodException {
    Method method = new DoubleCacheServiceImpl().getClass().getMethod("testCacheable", Student.class);
    Object[] cusArgs = new Object[1];
    cusArgs[0] = Student.builder()
            .sId(666)
            .sName("测试name")
            .build();
    Object value = PARSER.parseExpression("#student.sId+'-'+#student.sName")
            .getValue(new MethodBasedEvaluationContext(null, method, cusArgs, NAME_DISCOVERER));
    System.err.println("SPEL表达式解析出来的内容为:"+value);
}

在这里插入图片描述

具体的参数列个说明吧:

  • MethodBasedEvaluationContext:方法上下文,值是从中获取的
  • method:被解析的方法
  • cusArgs:被解析方法中的参数值
  • SpelExpressionParser:Spring 提供的 SPEL 解析包
  • DefaultParameterNameDiscoverer:用的默认,没深揪源码

缓存切面(如何设计?)

考虑到缓存一致性,以及注解的泛用性,其实这里面的代码要实现高可用还是有点难度的。首先我们要对增、删、改、查 操作都写对应的逻辑。例如查 db ,放缓存,此时 db 更新数据,为了保证 db 与缓存一致性,还需同步删除缓存,然后更新缓存。当然我这里不是专门开发消息中间件的,写本文的目的更多的是在于,让大家知道如何进行设计一个二级缓存框架。考虑到现在是简洁开发的天下,结合之前看 Spring 自动装配的源码,自己手撸一个 jar 包封装所有的逻辑,让大家只需导入 jar 包,就可以调用我定义的注解完成二级缓存查询。

/**
 * aop 环绕通知
 */
@Slf4j
public class DoubleCacheInterceptor implements MethodInterceptor {
    private AnalysisKeyCache analysisKeyCache;

    public DoubleCacheInterceptor(AnalysisKeyCache analysisKeyCache) {
        this.analysisKeyCache = analysisKeyCache;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        DoubleCacheAble doubleCache = getDoubleCache(invocation.getMethod());
        if(doubleCache==null) return invocation.proceed();
        String realKey = String.valueOf(getRealKey(invocation.getArguments(), invocation.getMethod(),
                doubleCache.key(), doubleCache.value()));
        Object cacheValue = analysisKeyCache.get(realKey);
        if (null != cacheValue) return cacheValue;
        Object proceed = invocation.proceed();
        analysisKeyCache.put(realKey, doubleCache.unLess(), proceed);
        return proceed;
    }

    public DoubleCacheAble getDoubleCache(Method method) {
        DoubleCacheAble targetDataSource = method.getAnnotation(DoubleCacheAble.class);
        if (targetDataSource == null) {
            Class<?> declaringClass = method.getDeclaringClass();
            targetDataSource = declaringClass.getAnnotation(DoubleCacheAble.class);
        }
        return targetDataSource;
    }

    public Object getRealKey(Object[] cusArgs, Method method, String key, String value) {
        Object realKey = value + new SpelExpressionParser().parseExpression(key)
                .getValue(new MethodBasedEvaluationContext(null, method, cusArgs, new DefaultParameterNameDiscoverer()));
        log.info("{} SPEL表达式解析得到的完整key: {}", method.getName(), realKey);
        return realKey;
    }
}

想到切面大家可能第一时间想到的是用 @Aspect+@Around 实现,但是对于开源项目来说,所有轮子都是自己造的,为什么还要用轮子拼轮子呢?况且由于切面过多,可能导致我们自己的切面无法第一时间执行这也是个问题, 因此我这里采用 MethodInterceptor (方法拦截器)方法实现 AOP 拦截。

缓存 CRUD 如何设计?(使用委派模式)

由于我们要用到 CaffeineCache+Redis 这俩种缓存,考虑到代码解耦,决定用委派模式实现。此处借鉴 Mybatis 二级缓存源码中的设计,利用委派模式将日志缓存、序列化缓存、LRU缓存、定时缓存、持久化缓存代码各自抽离出来,实现解耦的目的。在这里插入图片描述
为此我设计了如下四个缓存,当一个查询请求过来会先经过 AnalysisKeyCache 隐式的为 key 添加前缀,然后经过 SerializeCache 依次从 MRCaffeineCache 、RedisCache 获取值,最后将值反序列化给我们。看懂了我的这段代码,再去看 Mybatis 获取二级缓存的源码将十分简单。

  1. AnalysisKeyCache:为 key 加统一前缀
  2. SerializeCache:缓存 value 值转换成 byte 数组存储
  3. MRCaffeineCache:CaffeineCache 本地缓存(CRUD)
  4. RedisCache:Redis 缓存(CRUD)
    在这里插入图片描述

整合自动装配

要想实现让大家开箱即用第三方 jar 包,自动装配少不了。在 resources 目录下创建一个 MATE_INF 文件夹,放入一个 spring.factories 文件,里面的内容 org.springframework.boot.autoconfigure.EnableAutoConfiguration 这段是固定的,Value 值代表要变成 Bean 的类。当 jar 引入各自项目中来时这些类就会变成项目中的 Bean。至于为什么推荐大家阅读自动装配的源码,本文不做过多阐述。
然后编写 MRCacheAutoConfiguration 类,约定哪些类要变成 Bean 即可。
在这里插入图片描述

Redis 过期 Key 如何处理?

存在这么一种情况就是二级缓存数据过期了,一级换存还有数据,为了保证缓存一致性,此时需监听过期 Key ,同步删除一级缓存。那么有人会说了,一级缓存过期不要删除二级缓存吗,一级缓存本就是为了缓解二级缓存压力而设计的,且为内存,一级缓存过期了无需做任何操作,毕竟二级缓存才是我们的兜底。

查缓存测试

新建一个项目引入我们的 MRCache 包,使用其提供的 @DoubleCacheAble 缓存,编写对应的测试 Service即可。做到 0 代码入侵。

在这里插入图片描述

调用接口查询数据,发现第一次查十分缓慢,第二次查很快走了缓存。

在这里插入图片描述

全局的 key 前缀也正常被设置。逻辑在 AnalysisKeyCache 里面,这里不做阐述了。

在这里插入图片描述

再次查询直接走的内存缓存了。

在这里插入图片描述

过期 Key 清除测试

手动修改 Redis 数据的 TTL 为 1,过一秒成功触发我们的监听方法执行里面的逻辑

在这里插入图片描述

推荐阅读

手把手debug自动装配源码、顺带弄懂了@Import等相关的源码(全文3w字、超详细)

深入mybatis源码解读~手把手带你debug分析源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/666377.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux centos7下漏洞扫描工具 Nessus8.15.9的下载、安装

一、下载Nessus 传送带地址&#xff1a;Download Nessus | Tenable 因为Darren洋的Linux操作系统是Linux Centos7 64 位&#xff0c;大家可以根据自己的选择合适的系统版本&#xff0c;在linux系统中用以下命令即可完成查询系统版本。 cat /etc/redhat-release 二、安装Ness…

Axure8 基本操作记录

参考&#xff1a;黑马产品经理课程 视频资源&#xff1a;day1&day2&#xff0c;Axure部分 文章小结图片 Axure8常用功能 选择/缩放 选择 包含选中&#xff1a;全部选中才有效&#xff08;避免误操作&#xff0c;建议使用这个&#xff09;相交选中&#xff1a;相交即全选中…

同时安装vue-cli2和vue-cli3

同时安装vue-cli2和vue-cli3 发布时间环境安装后的效果安装vue-cli2安装vue-cli3vue-cli3和vue-cli2的区别vue-cli2目录结构vue-cli3目录结构 发布时间 vue版本发布时间Seed.js2013年vue最早版本最初命名为Seedvue-js 0.62013年12月更名为vuevue-js 0.82014年1月对外发布vue-j…

vue2 用watch监听props 失效,解决办法

这个是父组件传递下来的props 这样子好像TCshow的值并没有赋上 必须修改成下面这种&#xff1a;

[golang 微服务] 7. go-micro框架介绍,go-micro脚手架,go-micro结合consul搭建微服务案例

一.go-micro框架 前言 上一节讲解了 GRPC微服务集群 Consul集群 grpc-consul-resolver相关的案例,知道了微服务之间通信采用的 通信协议&#xff0c;如何实现 服务的注册和发现&#xff0c;搭建 服务管理集群&#xff0c;以及服务与服务之间的 RPC通信方式,具体的内容包括: pro…

SpringBoot 如何使用 IOC 容器

SpringBoot 如何使用 IOC 容器 Spring 是一个非常流行的 Java 开发框架&#xff0c;它提供了一个强大的 IoC&#xff08;Inversion of Control&#xff09;容器来管理 Java 对象之间的依赖关系。在 SpringBoot 中&#xff0c;我们可以非常方便地使用这个 IoC 容器来管理我们的…

骨传导耳机音质怎么样,几款解析力度不错的骨传导耳机分享

​骨传导耳机在之前的时候一直是“冷门”的&#xff0c;但是随着技术的进步&#xff0c;现在骨传导耳机也逐渐被大家所熟知。对于喜欢运动和健身的人来说&#xff0c;骨传导耳机可以避免佩戴普通耳机导致耳朵疼痛的情况。因此&#xff0c;目前在市面上很多骨传导耳机都很受欢迎…

Git教程(快速上手,超详细)

文章目录 版本控制Git环境配置Git基本理论Git项目搭建Git文件操作使用码云IDEA集成GitGit分支 版本控制 版本迭代:每次更新就会有新的版本&#xff0c;旧的版本需要保留。所以我们需要一个版本控制工具帮助我们处理这个问题 版本控制&#xff08;Revision control&#xff09;是…

入门学习编码器与自编码器1----包括详细的理论讲解与详细的python程序代码,小白直接看懂!!!纯干货

文章目录 前言--为什么要学习编码器和自编码器&#xff1f;一、编码器与自编码器究竟是什么&#xff1f;二、下面是一个简单的Python实现自编码器的示例三、程序运行结果四、查看模型结构总结 前言–为什么要学习编码器和自编码器&#xff1f; 学习编码器和自编码器可以帮助我…

【数据分享】1929-2022年全球站点的逐月平均风速数据(Shp\Excel\12000个站点)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 对于具体到监测站点的气象数据&#xff0c;之前我们分享过1929-2022年全球气象…

「你将购买的是虚拟内容服务,购买后不支持退订」,真的合理么?

编辑导语&#xff1a;你是否也有见过相似提示&#xff0c;即虚拟内容服务购买之后不予退款&#xff1f;那么你有想过&#xff0c;在这一规定背后&#xff0c;其制约因素都有什么吗&#xff1f;这一规定是合理的吗&#xff1f;用户若真的有退款需求&#xff0c;产品上是否能实现…

卷积计算加速方法--分块卷积1

文章目录 1、大尺寸卷积存在的问题2、分块卷积overlap产生的来源3、分块卷积overlap的计算4、结论及加速效果 1、大尺寸卷积存在的问题 当卷积的输入太大导致内存不够用时&#xff0c;考虑将一大块卷积分成多个小块分别进行卷积&#xff0c;相当于将原始输入分成几个小的输入经…

【C++】C++11:线程库和包装器

C11最后一篇文章 文章目录 前言一、线程库二、包装器和绑定总结 前言 上一篇文章中我们详细讲解了lambda表达式的使用&#xff0c;我们今天所用的线程相关的知识会大量的用到lambda表达式&#xff0c;所以对lambda表达式还模糊不清的可以先将上一篇文章看明白。 一、线程库 在…

域名解析详解

域名解析 记录类型&#xff1a; 提示&#xff1a; 将域名指向云服务器&#xff0c;选择 A&#xff1b; 将域名指向另一个域名&#xff0c;选择 CNAME&#xff1b; 建立邮箱选择 MX&#xff0c;根据邮箱服务商提供的 MX 记录填写。 记录类型解释A用来指定域名的 IPv4 地址&…

燃气管网监测设备:燃气管网压力在线监测

燃气作为一种重要的能源&#xff0c;广泛用于家庭、工业和商业领域。然而&#xff0c;燃气管网系统在运输和分配过程中可能面临压力波动、管道老化、外部破坏等问题&#xff0c;可能导致燃气泄漏和事故发生。燃气管网压力在线监测是保障燃气管网安全运营的重要手段之一。通过燃…

Linux系统之部署Homepage个人导航页

Linux系统之部署Homepage个人导航页 一、Homepage介绍1.1 Homepage简介1.2 Homepage主要特点 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本3.3 检查系统是否安装Node.js 四、部署Node.js 环境4.1 下载Node…

感谢ChatGPT,救了我狗的命!

前一段时间&#xff0c;国外一位小哥哥在推特上发布了一条消息&#xff0c;声称GPT-4拯救了自家狗狗的性命。 这是怎么一回事呢&#xff1f; 这个小哥哥养了一只两岁的边境牧羊犬&#xff0c;这只牧羊犬被诊断出患有蜱传疾病&#xff0c;这属于一种细菌性传染病。 虽然小哥哥一…

30分钟吃掉DQN算法

表格型方法存储的状态数量有限&#xff0c;当面对围棋或机器人控制这类有数不清的状态的环境时&#xff0c;表格型方法在存储和查找效率上都受局限&#xff0c;DQN的提出解决了这一局限&#xff0c;使用神经网络来近似替代Q表格。 本质上DQN还是一个Q-learning算法&#xff0c;…

金九银十预备秋招: 大厂面试必考点及 Java 面试框架知识点整理

Java 面试 “金九银十”这个字眼对于程序员应该是再熟悉不过的了&#xff0c;每年的金九银十都会有很多程序员找工作、跳槽等一系列的安排。说实话&#xff0c;面试中 7 分靠能力&#xff0c;3 分靠技能&#xff1b;在刚开始的时候介绍项目都是技能中的重中之重&#xff0c;它…

龙膜公益“聚光行动”再起航 为云南山区小学援建绿色电脑教室

中国&#xff0c;上海——近日&#xff0c;全球汽车膜品牌龙膜的公益活动“为山区学校援建绿色电脑教室”在云南泸西县再度起航。为当地的“阿盈里小学”和“歹鲁小学”添置了2间电脑教室&#xff0c;配备了82台再生电脑&#xff0c;为600多名学生提供了数字化设备的使用机会&a…