构建以caffeine为L1,Redis为L2的多级缓存

在这里插入图片描述

  • 🏃‍♂️ 微信公众号: 朕在debugger
  • © 版权: 本文由【朕在debugger】原创、需要转载请联系博主
  • 📕 如果文章对您有所帮助,欢迎关注、点赞、转发和订阅专栏!

前言

S(Situation):业务代码与缓存逻辑交织在一起,耦合度太高,后期维护人员无法快速接手。

T(Task):预期实现业务代码与缓存逻辑解耦,使得业务人员可以专注于业务代码编写,在此基础上仍要保障数据一致性。
A(Action):利用 Spring Cache + Redis,接管Spring 的 CacheManager。
R(Result):实现业务代码与缓存逻辑解耦,业务人员仅用注解即可完成对目标对象的缓存实现。



文章目录

  • 一、业务代码与缓存逻辑紧密相连
  • 二、多级缓存执行逻辑
  • 三、如何利用 Spring Cache + Redis,接管 Spring 的 CacheManager
  • 四、核心代码
    • 自定义 CacheManager 多级缓存实现
    • 多级缓存查询实现
  • 五、实战
    • 5-1、项目打包后在测试模块引入其依赖
    • 5-2、项目启动类加注解 @EnableCaching
    • 5-3、配置 redis 属性
    • 5-4、接口使用注解实现缓存与业务逻辑解耦
  • 六、总结


Tips:
读者可参考项目代码,构建自己的一个 stater,因文中引入作者私有maven仓库,故此 demo 打包会出现 error,找不到对应依赖,但这不影响读者理解本文思想。


一、业务代码与缓存逻辑紧密相连

先来看一段类似场景的代码

public Product getProductById(int productId) {
        Product product = null;
        // 先尝试从缓存中获取商品信息
        String cachedProduct = getFromCache(productId);
        if (cachedProduct != null) {
            // 如果缓存命中,直接从缓存中获取商品对象
            product = deserializeProduct(cachedProduct);
            System.out.println("Fetched product " + productId + " from cache.");
        } else {
            // 如果缓存中没有,则从数据库中获取商品信息
            product = fetchProductFromDatabase(productId);
            if (product != null) {
                // 将获取到的商品信息存入缓存,有效期设置为1小时
                putInCache(productId, serializeProduct(product));
                System.out.println("Cached product " + productId + " in cache.");
            }
        }
        return product;
    }

怎么样?是不是感觉也没什么大不了的?上面只是一个举例,真实情况业务复杂多了…

如果优化成下面这样子,是不是瞬间心情大好?

@Cacheable(value = "product",key = "#productId")
public Product getProductById(int productId) {
        Product product = fetchProductFromDatabase(productId);
        return product;
    }

二、多级缓存执行逻辑

先看图示一吧,可以很快明白这个逻辑是如何运行的。

▲图一 / L1&L2执行过程

再看看图示二,相比于图示一会更加详细地了解到执行细节。

▲图二 / L1&L2细节执行过程

三、如何利用 Spring Cache + Redis,接管 Spring 的 CacheManager

先看图示三吧,大致脉络就是这样子

▲图三 / 核心代码关系结构

四、核心代码

源码可参考 https://gitee.com/csnz/cache-spring-boot-starter

自定义 CacheManager 多级缓存实现

public class RedisCaffeineCacheManager implements CacheManager{
    private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>();
    private CacheConfigProperties cacheConfigProperties;
    private RedisTemplate<Object, Object> stringKeyRedisTemplate;
    // 是否动态根据cacheName创建Cache的实现
    private boolean dynamic;
    // 当前节点存储的缓存对象集合名称
    private Set<String> cacheNames;
    // 当前节点id
    private Object serverId;
	/*
     * @Description:获取指定名称的缓存对象
     * 如果 cacheMap 中已存在,则直接返回;
     * 如果 cacheMap 中不存在且不允许动态创建缓存,则返回 null;
     * 如果不存在且允许动态创建缓存,则调用 createCache 方法创建缓存并放入 cacheMap 中
     * @Param:[name]
     * @Return org.springframework.cache.Cache
     */
    @Override
    public Cache getCache(String name) {
        Cache cache = cacheMap.get(name);
        if (cache != null) {
            return cache;
        }
        if (!dynamic && !cacheNames.contains(name)) {
            return null;
        }
        cache = createCache(name);
        Cache oldCache = cacheMap.putIfAbsent(name, cache);
        log.debug("create cache instance, the cache name is : {}", name);
        return oldCache == null ? cache : oldCache;
    }
}

多级缓存查询实现

/**
 * @Author:CSNZ
 * @Description:自定义的缓存实现类,结合了 Redis 和 Caffeine 两种缓存机制的优点
 * @Version:1.0
 **/
@Slf4j
@Getter
public class RedisCaffeineCache extends AbstractValueAdaptingCache implements Cache<Object, Object> {
    private final String name; // 缓存名称,例如 externalApiData

    private final Cache<Object, Object> caffeineCache;

    private final RedisTemplate<Object, Object> stringKeyRedisTemplate;

    private final String cachePrefix;

    private final String getKeyPrefix;

    private final Duration defaultExpiration;

    private final Duration defaultNullValuesExpiration;

    private final Map<String, Duration> expires;

    private final String topic;

    private final Object serverId;

    private final Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap<>();
	 /**
     * 检查L1或L2缓存中是否存在键,不存在则返回 null
     * @param key
     * @return
     */
    @Override
    protected Object lookup(Object key) {
        // 根据前缀拼接 key
        Object cacheKey = getKey(key);
        // 从 L1 中查找此 key
        Object value = getCaffeineValue(key);
        if (Objects.nonNull(value)) {
            log.debug("get cache from caffeine, the key is : {}", cacheKey);
            return value;
        }
        // L1 中查无此key,改从 L2 中查找
        value = getRedisValue(key);
        if (value != null) {
            log.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
            setCaffeineValue(key, value);
        }
        return value;
    }
}

五、实战

5-1、项目打包后在测试模块引入其依赖

<dependency>
    <groupId>com.csnz</groupId>
    <artifactId>cache-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

5-2、项目启动类加注解 @EnableCaching

@SpringBootApplication
@EnableCaching
public class TestServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(TestServiceApplication.class, args);
    }
}

5-3、配置 redis 属性

server:
  port: 80
spring:
  data:
    redis:
      database: 0
      host: IP  #Redis服务器地址
      port: 6379            #Redis服务器连接端口
      password: password   #Redis服务器连接密码(默认为空)
      timeout: 8000

5-4、接口使用注解实现缓存与业务逻辑解耦

@RestController
public class HelloController {
    public static final String prefix = "REAL:";

    // 在方法执行前检查缓存是否存在预期的值,如果存在则直接返回缓存中的值,避免重复执行方法
    @Cacheable(value = "user",key = "#name")
    @GetMapping("/getRealName/{name}")
    public String getRealNameFromCache(@PathVariable("name") String name) throws InterruptedException {
        Thread.sleep(3000);
        return prefix+name;
    }
    // 在方法执行后强制将返回值存入缓存,以保证缓存中的值是最新的
    @CachePut(value = "user",key = "#user.id")
    @PostMapping("/updateUser")
    public User updateUser(@RequestBody User user){
        return user.setName("CSNZ");
    }
    // 从缓存中移除指定的缓存条目(剔除此缓存对象)
    @CacheEvict(value = "user",allEntries = true)
    @GetMapping("/clearAndDel")
    public String delUserCache(){
        return "clear all successfully!";
    }
    // 从缓存中移除指定的缓存条目
    @CacheEvict(value = "user",key = "#name")
    @GetMapping("/clearByName/{name}")
    public String clearUserCacheByName(@PathVariable("name") String name){
        return String.format("clear %s cache successfully!",name);
    }
    // @Caching 注解允许在一个方法上同时应用多个 Spring 缓存注解,以提供更细粒度的缓存控制
    @Caching(
            cacheable = {
                @Cacheable(value = "user",key = "#user.id")
            },
            evict = {
                // 清除指定缓存区域("user")中的所有缓存条目,表示会将整个缓存区域清空,即删除缓存中的所有内容,并不仅仅是移除所有条目
                @CacheEvict(value = "employee",allEntries = true)
            }
    )
    @PostMapping("/complex")
    public void complexThing(@RequestBody User user){
        // do something...
    }
}

六、总结

以上案例就是利用 Spring Cache + Redis,接管 Spring 的 CacheManager,实现业务代码与缓存逻辑解耦的一个简要过程,具体细节还需要深入代码理解思想。




finally

如果大家觉得本文写得不错,别忘了给个赞哦!同时,如果您有任何疑问或建议,欢迎在评论区留言,让我们一起交流、探讨!

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

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

相关文章

RK3588 Android13 TvSetting 中增加 WebView 切换菜单

前言 电视产品,客户要求在设置中设备偏好设置子菜单下增加一个 WebView切换菜单,一开始不知道怎么下手,后来想起来在设置开发者选项里有一个类似的菜单, 去把实现逻辑搞出来应该就ok。 效果图 TvSetting 部分修改文件清单 packages/apps/TvSettings/Settings/res/values…

改机软件有哪些?实现一键新机、改串号、改IMEI和手机参数的需求 硬改手机软件,新机环境模拟 设备伪装,一键改机,一键复原

这次针对可以直接开端口修改参数的机型做一些工具解析 前面接触合作过很多工作室。其中很多工作室对于各自软件的跳验证有各自的需求。 一个机型各项参数一般有IMEI WiFi 蓝牙 sn psb ESN等等。 针对这些参数的修改首先要明白各自软件检测的具体是哪些参数来验证。 对于常用…

在Ubuntu上配置PPPoE服务:从安装到自动化启动的全指南

在Ubuntu上配置PPPoE服务&#xff1a;从安装到自动化启动的全指南 PPPoE&#xff08;点对点协议以太网&#xff09;是一种广泛用于DSL和光纤宽带连接的协议。在本篇技术博客中&#xff0c;我们将详细介绍如何在Ubuntu系统上配置PPPoE服务&#xff0c;包括安装、配置、启动以及…

STM32——使用TIM输出比较产生PWM波形控制舵机转角

一、输出比较简介&#xff1a; 只有高级定时器和通用寄存器才有输入捕获/输出比较电路&#xff0c;他们有四个CCR&#xff08;捕获/比较寄存器&#xff09;&#xff0c;共用一个CNT&#xff08;计数器&#xff09;&#xff0c;而输出比较功能是用来输出PWM波形的。 红圈部分…

深入探索大模型的魅力:前沿技术、挑战与未来展望

目录 一、大模型的前沿技术 二、大模型面临的挑战 三、大模型的未来展望 四、总结 在当今人工智能领域&#xff0c;大模型不仅是一个热门话题&#xff0c;更是推动技术进步的重要引擎。从深度学习的浪潮中崛起&#xff0c;大模型以其卓越的性能和广泛的应用前景&#xff0c…

中医对于帕金森病的病因和症状有何解释?

中医对帕金森病的病因解释 中医认为帕金森病的病因复杂多样&#xff0c;涉及多个方面。首先&#xff0c;精神因素如长期的情绪抑郁、悲伤、忧虑等精神不畅可能导致气机郁结&#xff0c;气血运行障碍&#xff0c;进而影响脑部神经系统的功能。其次&#xff0c;肝郁气滞也被认为…

2025艺考时间线来啦!所有艺考生码住!

2025届艺考生们的征途即将启程。对于每一个即将参加艺考的考生和家长来说&#xff0c;梳理艺考时间节点是尤为重要的。 对于艺考生而言&#xff0c;更早的规划意味着更充分的准备时间&#xff0c;更扎实的专业能力。补齐艺考信息差&#xff0c;以下2025艺考时间线一定要看明白…

CC7关于ConstantTransformer返回值不能和put一样的分析

CC7关于ConstantTransformer返回值不能和put一样的分析 前言 实验室的gaorenyusi也是学到cc7的时候问了我一个很好的问题&#xff0c;我当时学的时候没有在意&#xff0c;然后就去调试分析解决了一下 分析 首先是paylaod package CC7;import org.apache.commons.collectio…

Mysql基本知识点

1.数据库的基本操作 显示当前的数据库 show databases;创建一个数据库 直接创建数据库 create database 数据库名字;如果系统没有 test2 的数据库&#xff0c;则创建一个名叫 test2 的数据库&#xff0c;如果有则不创建 create database if not exists test2;如果系统没有 db…

Mathematica训练课(44)-- 一些符号#,,//, /. 的整理

①“//”在后面写成你要执行的操作,即可执行。 注意:这一函数作用域标志的优先级是很靠后的,也就是说它会对一整行式子作用。 ②@的作用是在@后面的第一个元素进行操作 Sqrt @ a(*@作用在@后面、对离@最近的仅仅一个元素作用*) 例如,下面 若作用对象外面套着{},那么就要…

学校消防设施设备管理系统

建立和落实校园消防安全管理责任制,做到消防安全工作有人专管,部门和岗位有人落实的日常管理&#xff0c;及时发现消防安全隐患,及时反映,及时处理,杜绝校园内消防安全隐患。 凡尔码平台搭建学校消防设施设备管理系统可以通过设备管理系统对消防器材设施基本信息、设施有效期、…

Unity2D - 状态机(State Machine)详解

1. 状态机概述 在角色的生成中&#xff0c;由于事件的不同&#xff0c;动作的不同&#xff0c;角色会处于不同的状态中。例如对战冒险游戏&#xff0c;面临Boss的攻击&#xff0c;角色会受到例如中毒&#xff0c;恐惧等Debuff效果&#xff0c;若单纯的在一个脚本中使用if等语句…

中霖教育靠谱吗?在职备考一建好通过吗?

中霖教育靠谱吗?在职备考一建好通过吗? 课程设置&#xff1a;报名后会进行测评&#xff0c;了解学员的知识掌握情况、时间安排和记忆思维特点等&#xff0c;制定更适合的学习计划。 课程以考试通过为目标&#xff0c;去繁化简&#xff0c;只讲有用的干货&#xff0c;帮助快…

ASUS/华硕幻14 2023 GA402X系列 原厂Windows11-22H2系统

安装后恢复到您开箱的体验界面&#xff0c;带原机所有驱动和软件&#xff0c;包括myasus mcafee office 奥创等。 最适合您电脑的系统&#xff0c;经厂家手调试最佳状态&#xff0c;性能与功耗直接拉满&#xff0c;体验最原汁原味的系统。 原厂系统下载网址&#xff1a;http:…

python基础语法 004-1流程控制- 条件控制

1 条件控制 1.1 表达 条件表达式冒号缩进 1.1.1 单个条件&#xff1a;满足表达式 """ ############if的表示 if 条件表达式:(缩进)条件满足以后要运行的代码例子: #遇到冒号要缩进 #缩进&#xff1a;1个缩进用4个空格&#xff0c;整个篇幅缩进需要统一 #4个…

如何解决三菱软件提示 起动MELSOFT Mediative Server失败

前言&#xff1a; 注意&#xff0c;这篇文章仅针对如何解决 起动MELSOFT Mediative Server失败 的问题。对于其他相关的问题&#xff0c;请搜索其他相应的解决办法。 本人是在重装三菱GX Works软件时遇到此问题的。后来搜索发现无人能妥善的关闭这个提示。因此本文介绍如何关…

关于多媒体本地化准备的小清单

多媒体本地化需要翻译多媒体材料&#xff08;音频、视频、动画等&#xff09;&#xff0c;同时考虑到这些材料所针对的国家的文化特征。 多媒体材料能快速有效地将思想传达给目标受众。它们表达了人们的情感&#xff0c;比纯文本更令人难忘。然而&#xff0c;它们的影响取决于…

pdf怎么转换成jpg,本地转换还是在线转换?

PDF&#xff08;Portable Document Format&#xff09;和JPG&#xff08;Joint Photographic Experts Group&#xff09;这两种文件格式在我们的日常生活和工作中扮演着举足轻重的角色。PDF因其跨平台、保持原样性强的特点&#xff0c;被广泛应用于文件传输和存储&#xff1b;而…

优维“统一开放平台”:开放、开发、集成、客制化

基于丰富完善的产品体系&#xff0c;优维重磅推出了统一开放平台。这款由优维自主设计与研发&#xff0c;集数据开发、能力开放、能力集成、客制化为一体的统一开放平台&#xff0c;具备应用市场、应用开发、连接能力、采控平台、API集市、开发者工具等功能模块&#xff0c;可为…

办公效率新高度:利用办公软件实现文件夹编号批量复制与移动,轻松管理文件

在数字化时代&#xff0c;我们的工作和生活都围绕着海量的数据和文件展开。然而&#xff0c;随着数据量的不断增加&#xff0c;如何高效地管理这些数字资产成为了摆在我们面前的一大难题。今天&#xff0c;我要向您介绍一种革命性的方法——利用办公软件实现文件夹编号批量复制…