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

本文目录

    • 前言
    • 本文术语
    • 本文项目地址
    • 设计思路
    • 开发思路
    • @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;@Beanpublic 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;}@Beanpublic 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;}@Overridepublic 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.hqwc.cn/news/2139.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

async异步任务_同步任务选项

需要先看完上文&#xff1a;async创建异步任务_御坂美琴1的博客-CSDN博客 让类里面的一个成员函数当作线程的参数。 async里面有三个参数&#xff0c;一个是成员函数的地址&#xff0c;第二个是 类&#xff0c;第三个是传入的参数。 接下来介绍async的同步线程创建。 asy…

华为云Classroom一站式教学实践平台,开启云端教学新征程

随着高考落下帷幕&#xff0c;各高校将迎来新一届大学新生入学&#xff0c;他们的学长学姐们经过四年的学习&#xff0c;也即将步入社会&#xff0c;迈向一段新的人生旅程。 在这里小智先祝大家未来一切顺意&#xff0c;不忘初心&#xff0c;大鹏一日同风起&#xff0c;扶摇直…

Android跨平台语言分析

跨平台技术发展的三个阶段 第一阶段是混合开发的web容器时代 为了解决原生开发的高成本、低效率&#xff0c;出现了Hybrid混合开发原生中嵌入依托于浏览器的WebViewWeb浏览器中可以实现的需求在WebView中基本都可以实现但是Web最大的问题是&#xff0c;它的性能和体验与原生开发…

需求分析六步法

需求收集可能看起来不言自明&#xff0c;但它很少得到应有的充分关注。就像运动前伸展或睡前刷牙一样&#xff0c;这是一项经常被忽视的简单任务。 但是&#xff0c;忽视这些看似简单的事情的后果可能会导致伤害、蛀牙&#xff0c;或者在项目管理的情况下&#xff0c;导致项目…

qt调用图片并自适应界面大小显示

一、前言 记录qt使用图片打开、查看和关闭等操作 实现点击按键&#xff0c;打开选择的指定位置图片&#xff0c;有缩放显示&#xff0c;并可以点击放大按键放大图片&#xff0c;放大后图片自适应电脑窗口大小&#xff0c;大于窗口尺寸会根据最大宽和高缩放&#xff0c;小于窗…

【系统开发】尚硅谷 - 谷粒商城项目笔记(五):分布式缓存

文章目录 分布式缓存缓存使用场景redis作缓存中间件引入redis依赖配置redis堆外内存溢出 缓存失效问题缓存穿透缓存雪崩缓存击穿 Redisson分布式锁导入依赖redisson配置类可重入锁读写锁缓存一致性解决 缓存-SpringCache简介Cacheable自定义缓存配置CacheEvictCachePut原理与不…

Redis主从/哨兵机制原理介绍

目录 ​编辑 一、主从复制 1.1 什么是主从复制 1.2 主从复制的作用 1.3 主从复制原理 1.3.1 全量复制 1.3.2 增量复制 1.3.3 同步流程 二、哨兵机制 2.1 哨兵机制介绍 2.1.1 集群逻辑图 2.1.2 哨兵机制实现的功能 2.2 哨兵机制原理 2.2.1 监控 2.2.2 下线 2.2.2.1 下线流程 2.…

HarmonyOS学习路之开发篇—AI功能开发(文档检测校正)

基本概念 文档校正提供了文档翻拍过程的辅助增强功能&#xff0c;包含两个子功能&#xff1a; 文档检测&#xff1a;能够自动识别图片中的文档&#xff0c;返回文档在原图中的位置信息。这里的文档泛指外形方正的事物&#xff0c;比如书本、相片、画框等。文档校正&#xff1a…

Midjourney使用教程:三 图片风格提示

这里我根据现在的官方文档来继续我们的Midjourney的教程&#xff0c;看到这里如果你去实践的话&#xff0c;估计你已经有了好多张属于自己的图片。 这时候你不在满足简单的提示生成的Midjourney的默认风格图片&#xff0c;实际上你可以通过一些关键词做提示&#xff0c;来改变…

pcl基于八叉树进行空间划分和搜索操作

建立空间索引在点云数据处理中已被广泛应用&#xff0c;常见空间索引一般是自顶向下逐级划分空间的各种空间索引结构&#xff0c;比较有代表性的包括 BSP 树、KD 树、KDB 树、 R树、R树、CELL 树、四叉树和八叉树等索引结构&#xff0c;而在这些结构中 KD 树和八叉树在 3D点云数…

多元分类预测 | Matlab灰狼算法(GWO)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,GWO-HKELM分类预测

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元分类预测 | Matlab灰狼算法(GWO)优化混合核极限学习机(HKELM)分类预测,多特征输入模型,GWO-HKELM分类预测 多特征输入单输出的二分类及多分类模型。程序内注释详细,直接替换数据就可以用。程序语言为matlab…

【redis】redis集群

这里是redis系列文章之《redis集群》&#xff0c;上一篇文章链接&#xff1a;【redis基础】哨兵_努力努力再努力mlx的博客-CSDN博客 目录 概念 作用 集群算法-分片-槽位slot 槽位与分配的概念及两者的优势 官网介绍分析 槽位 分片 两者的优势 slot槽位映射的三种解决方…