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;

    @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分析源码

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

二十三种设计模式第十二篇--组合模式

组合模式是一种结构型设计模式&#xff0c;它允许将对象组合成树形结构来表示整体-部分的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。 在组合模式中&#xff0c;有两种类型的对象&#xff1a;叶子对象和组合对象。叶子对象表示树结构中的叶子节点&…

Matplotlib---雷达图

1. 雷达图 fig plt.figure(figsize(6, 6))x np. linspace(0, 2*np.pi, 6, endpointFalse) y [83, 61, 95, 67, 76, 88]# 保证首位相连 x np.concatenate((x, [x[0]])) y np.concatenate((y, [y[0]]))# 雷达图 axes plt.subplot(111, polarTrue) axes.plot(x, y, o-, l…

第十六届CISCN复现MISC——国粹

国粹 不是我说&#xff0c;我当时比赛的时候&#xff0c;在那里叭叭叭的数的老用心了结果他是一道非常不常规的图片密码题&#xff0c;又是一种我没见过的题型 看了一些大佬的解题&#xff0c;知道他是一个坐标类型的图片拼凑 发现很多都提到了opencv&#xff0c;又是一个知识…

考研算法32天:桶排 【桶排序】

算法介绍 桶排 举个例子&#xff0c;一个数组中的数是&#xff1a;4 1 2 3 5&#xff0c; 然后桶排的顺序是&#xff1a;将每个数应该在的下标算出来&#xff0c;咋算呢&#xff1f;这我们就得考虑两种情况&#xff1a;假设我们设现在这个需要找到自己在数组里位置的数是x。…

【计算机网络】IP 地址处理函数

目录 1.struct sockaddr_in的结构 2.一般我们写的结构 3.常见的“点分十进制” 到 ” uint32_t 的转化接口 3.1. inet_aton 和 inet_ntoa &#xff08;ipv4&#xff09; 3.2. inet_pton 和 inet_ntop (ipv4 和 ipv6&#xff09; 3.3. inet_addr 和 inet_network 3…

哈工大计算机网络课程传输层协议之:拥塞控制原理剖析

哈工大计算机网络课程传输层协议之&#xff1a;拥塞控制原理剖析 哈工大计算机网络课程传输层协议详解之&#xff1a;可靠数据传输的基本原理 哈工大计算机网络课程传输层协议详解之&#xff1a;流水线机制与滑动窗口协议 哈工大计算机网络课程传输层协议详解之&#xff1a;T…

【裸机开发】EPIT 定时器 —— 按键消抖

实际工程中&#xff0c;不能直接通过延时来消抖 ! 这里我们采用定时器来消抖&#xff0c;这也是内核处理消抖的一种方式。 目录 一、基本原理 1、延时消抖的弊端 2、定时器消抖原理 二、按键消抖实现 1、按键中断 2、定时器中断 三、附加&#xff1a;按键 / 定时器中断初…

Qgis加载在线XYZ瓦片影像服务的实践操作

目录 背景 一、XYZ瓦片相关知识 1、xyz瓦片金字塔 2、 瓦片编号 3、瓦片访问 二、在Qgis中加载在线地图 1、Qgis版本 2、瓦片加载 3、地图属性预览 总结 背景 在做电子地图应用的时候&#xff0c;很常见的会提到瓦片&#xff08;tile&#xff09;的概念&#xff0c;瓦片…

【MySQL】MVCC是如何解决快照读下的幻读问题的

文章目录 LBCC当前读 MVCC隐藏列undo logRead View 总结 我们从上文中了解到InnoDB默认的事务隔离级别是repeatable read&#xff08;后文中用简称RR&#xff09;&#xff0c;它为了解决该隔离级别下的幻读的并发问题&#xff0c;提出了LBCC和MVCC两种方案。其中LBCC解决的是当…

信号链噪声分析11

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 提示&#xff1a;这里可以添加技术概要 如今的射频(RF)系统变得越来越复杂。高度的复杂性要求所有系统指标&#xff08;例如严格的 链接和噪声预算&#xff09;达到最佳性能。确保整个信号链的正确设计至关重要。而信…

如何了解(海外抖音TiKToK)与国内抖音的区别以及介绍

一、海外抖音TK平台的优势 自从抖音在中国大受欢迎后&#xff0c;海外也推出了海外版抖音TK平台。尽管两者都是视频分享平台&#xff0c;但它们在一些方面具有明显的区别和独特的优势。下面将详细介绍海外抖音TK平台的优势以及与国内抖音的区别性。 优势&#xff1a; 1. 多元…

三防工业平板在哪些行业中得到广泛应用?

随着科技的不断进步&#xff0c;工业平板正逐渐成为各行业中不可或缺的工具。其中&#xff0c;三防工业平板由于其卓越的耐用性和丰富的功能&#xff0c;在许多行业中得到了广泛的应用。本文将重点介绍三防工业平板在以下几个行业中的应用。 三防工业平板在物流行业中发挥着关键…

vue-router.esm.js:2248 Error: Cannot find module ‘@/views/dylife/ 报错解决

具体是展示 一直加载 控制台报找不到模块 webpack版本问题&#xff0c;webpack4 不支持变量方式的动态 import &#xff0c;新版本需要使用 require() 来解决此问题。 return () > import(/views/${view}) 改写成 return (resolve) > require([/views/${view}], reso…

【三层交换机】网络杂谈(16)之三层交换机技术

涉及知识点 什么是三层交换机&#xff0c;三层交换技术的由来&#xff0c;三层交换机&#xff0c;三层交换的应用范例。深入了解三层交换机技术。 原创于&#xff1a;CSDN博主-《拄杖盲学轻声码》&#xff0c;更多内容可去其主页关注下哈&#xff0c;不胜感激 文章目录 涉及知…

HBase(5):导入测试数据集

1 需求 将ORDER_INFO.txt 中的HBase数据集&#xff0c;我们需要将这些指令放到HBase中执行&#xff0c;将数据导入到HBase中。 可以看到这些都是一堆的put语句。那么如何才能将这些语句全部执行呢&#xff1f; 2 执行command文件 2.1 上传command文件 将该数据集文件上传到指…

6.5 指令与文件的搜寻

6.5.1 指令文件名的搜寻 在终端机模式当中&#xff0c;连续输入两次[tab]按键就能够知道使用者有多少指令可以下达。 which &#xff08;寻找“可执行文件”&#xff09; 这个指令是根据“PATH”这个环境变量所规范的路径&#xff0c;去搜寻“可执行文件”的文件名。所以&…

DETR系列:RT-DETR(一) 论文解析

论文&#xff1a;《DETRs Beat YOLOs on Real-time Object Detection》 2023.4 DETRs Beat YOLOs on Real-time Object Detection&#xff1a;https://arxiv.org/pdf/2304.08069.pdf 源码地址&#xff1a;https://github.com/PaddlePaddle/PaddleDetection/tree/develop/conf…

【Visual Studio】报错 ASSERT: “i >= 0 i < size()“,使用 C++ 语言,配合 Qt 开发串口通信界面

知识不是单独的&#xff0c;一定是成体系的。更多我的个人总结和相关经验可查阅这个专栏&#xff1a;Visual Studio。 这个 Bug 是我做这个工程时遇到的&#xff1a;【Visual Studio】Qt 的实时绘图曲线功能&#xff0c;使用 C 语言&#xff0c;配合 Qt 开发串口通信界面。 文…

javaweb学习2

p标签使用 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title> </head> <body> <!--p标签定义段落 p元素自动在其前后创建一段空白--> hello&#xff0c;world &l…

设计模式之访问者模式笔记

设计模式之访问者模式笔记 说明Iterator(访问者)目录访问者模式示例类图抽象访问者角色类抽象元素角色类宠物猫类宠物狗类自己类其他人类家类测试类 说明 记录下学习设计模式-访问者模式的写法。JDK使用版本为1.8版本。 Iterator(访问者) 意图:表示一个作用于某对象结构中的…