Spring Boot多级缓存实现方案

1.背景

缓存,就是让数据更接近使用者,让访问速度加快,从而提升系统性能。工作机制大概是先从缓存中加载数据,如果没有,再从慢速设备(eg:数据库)中加载数据并同步到缓存中。

所谓多级缓存,是指在整个系统架构的不同系统层面进行数据缓存,以提升访问速度。主要分为三层缓存:网关nginx缓存、分布式缓存、本地缓存。这里的多级缓存就是用redis分布式缓存+caffeine本地缓存整合而来。

平时我们在开发过程中,一般都是使用redis实现分布式缓存、caffeine操作本地缓存,但是发现只使用redis或者是caffeine实现缓存都有一些问题:

  • 一级缓存:Caffeine是一个一个高性能的 Java 缓存库;使用 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。优点数据就在应用内存所以速度快。缺点受应用内存的限制,所以容量有限;没有持久化,重启服务后缓存数据会丢失;在分布式环境下缓存数据数据无法同步;
  • 二级缓存:redis是一高性能、高可用的key-value数据库,支持多种数据类型,支持集群,和应用服务器分开部署易于横向扩展。优点支持多种数据类型,扩容方便;有持久化,重启应用服务器缓存数据不会丢失;他是一个集中式缓存,不存在在应用服务器之间同步数据的问题。缺点每次都需要访问redis存在IO浪费的情况。

综上所述,我们可以通过整合redis和caffeine实现多级缓存,解决上面单一缓存的痛点,从而做到相互补足。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址:https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址:https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

2.整合实现

2.1思路

Spring 本来就提供了Cache的支持,最核心的就是实现Cache和CacheManager接口。但是Spring Cache存在以下问题:

  • Spring Cache 仅支持单一的缓存来源,即:只能选择 Redis 实现或者 Caffeine 实现,并不能同时使用。
  • 数据一致性:各层缓存之间的数据一致性问题,如应用层缓存和分布式缓存之前的数据一致性问题。

由此我们可以通过重新实现Cache和CacheManager接口,整合redis和caffeine,从而实现多级缓存。在讲实现原理之前先看看多级缓存调用逻辑图:

2.2实现

首先,我们需要一个多级缓存配置类,方便对缓存属性的动态配置,通过开关做到可插拔。

@ConfigurationProperties(prefix = "multilevel.cache")
@Data
public class MultilevelCacheProperties {/*** 一级本地缓存最大比例*/private Double maxCapacityRate = 0.2;/*** 一级本地缓存与最大缓存初始化大小比例*/private Double initRate = 0.5;/*** 消息主题*/private String topic = "multilevel-cache-topic";/*** 缓存名称*/private String name = "multilevel-cache";/*** 一级本地缓存名称*/private String caffeineName = "multilevel-caffeine-cache";/*** 二级缓存名称*/private String redisName = "multilevel-redis-cache";/*** 一级本地缓存过期时间*/private Integer caffeineExpireTime = 300;/*** 二级缓存过期时间*/private Integer redisExpireTime = 600;/*** 一级缓存开关*/private Boolean caffeineSwitch = true;}

在自动配置类使用@EnableConfigurationProperties(MultilevelCacheProperties.class)注入即可使用。

接下来就是重新实现spring的Cache接口,整合caffeine本地缓存和redis分布式缓存实现多级缓存

package com.plasticene.boot.cache.core.manager;import com.plasticene.boot.cache.core.listener.CacheMessage;
import com.plasticene.boot.cache.core.prop.MultilevelCacheProperties;
import com.plasticene.boot.common.executor.plasticeneThreadExecutor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;import javax.annotation.Resource;
import java.util.Objects;
import java.util.concurrent.*;/*** @author fjzheng* @version 1.0* @date 2022/7/20 17:03*/
@Slf4j
public class MultilevelCache extends AbstractValueAdaptingCache {@Resourceprivate MultilevelCacheProperties multilevelCacheProperties;@Resourceprivate RedisTemplate redisTemplate;ExecutorService cacheExecutor = new plasticeneThreadExecutor(Runtime.getRuntime().availableProcessors() * 2,Runtime.getRuntime().availableProcessors() * 20,Runtime.getRuntime().availableProcessors() * 200,"cache-pool");private RedisCache redisCache;private CaffeineCache caffeineCache;public MultilevelCache(boolean allowNullValues,RedisCache redisCache, CaffeineCache caffeineCache) {super(allowNullValues);this.redisCache = redisCache;this.caffeineCache = caffeineCache;}@Overridepublic String getName() {return multilevelCacheProperties.getName();}@Overridepublic Object getNativeCache() {return null;}@Overridepublic <T> T get(Object key, Callable<T> valueLoader) {Object value = lookup(key);return (T) value;}/***  注意:redis缓存的对象object必须序列化 implements Serializable, 不然缓存对象不成功。*  注意:这里asyncPublish()方法是异步发布消息,然后让分布式其他节点清除本地缓存,防止当前节点因更新覆盖数据而其他节点本地缓存保存是脏数据*  这样本地缓存数据才能成功存入* @param key* @param value*/@Overridepublic void put(@NonNull Object key, Object value) {redisCache.put(key, value);// 异步清除本地缓存if (multilevelCacheProperties.getCaffeineSwitch()) {asyncPublish(key, value);}}/*** key不存在时,再保存,存在返回当前值不覆盖* @param key* @param value* @return*/@Overridepublic ValueWrapper putIfAbsent(@NonNull Object key, Object value) {ValueWrapper valueWrapper = redisCache.putIfAbsent(key, value);// 异步清除本地缓存if (multilevelCacheProperties.getCaffeineSwitch()) {asyncPublish(key, value);}return valueWrapper;}@Overridepublic void evict(Object key) {// 先清除redis中缓存数据,然后通过消息推送清除所有节点caffeine中的缓存,// 避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中redisCache.evict(key);// 异步清除本地缓存if (multilevelCacheProperties.getCaffeineSwitch()) {asyncPublish(key, null);}}@Overridepublic boolean evictIfPresent(Object key) {return false;}@Overridepublic void clear() {redisCache.clear();// 异步清除本地缓存if (multilevelCacheProperties.getCaffeineSwitch()) {asyncPublish(null, null);}}@Overrideprotected Object lookup(Object key) {Assert.notNull(key, "key不可为空");ValueWrapper value;if (multilevelCacheProperties.getCaffeineSwitch()) {// 开启一级缓存,先从一级缓存缓存数据value = caffeineCache.get(key);if (Objects.nonNull(value)) {log.info("查询caffeine 一级缓存 key:{}, 返回值是:{}", key, value.get());return value.get();}}value = redisCache.get(key);if (Objects.nonNull(value)) {log.info("查询redis 二级缓存 key:{}, 返回值是:{}", key, value.get());// 异步将二级缓存redis写到一级缓存caffeineif (multilevelCacheProperties.getCaffeineSwitch()) {ValueWrapper finalValue = value;cacheExecutor.execute(()->{caffeineCache.put(key, finalValue.get());});}return value.get();}return null;}/*** 缓存变更时通知其他节点清理本地缓存* 异步通过发布订阅主题消息,其他节点监听到之后进行相关本地缓存操作,防止本地缓存脏数据*/void asyncPublish(Object key, Object value) {cacheExecutor.execute(()->{CacheMessage cacheMessage = new CacheMessage();cacheMessage.setCacheName(multilevelCacheProperties.getName());cacheMessage.setKey(key);cacheMessage.setValue(value);redisTemplate.convertAndSend(multilevelCacheProperties.getTopic(), cacheMessage);});}}

缓存消息监听:我们通监听caffeine键值的移除、打印日志方便排查问题,通过监听redis发布的消息,实现分布式集群多节点本地缓存清除从而达到数据一致性。

消息体

@Data
public class CacheMessage implements Serializable {private String cacheName;private Object key;private Object value;private Integer type;
}

caffeine移除监听:

@Slf4j
public class CaffeineCacheRemovalListener implements RemovalListener<Object, Object> {@Overridepublic void onRemoval(@Nullable Object k, @Nullable Object v, @NonNull RemovalCause cause) {log.info("[移除缓存] key:{} reason:{}", k, cause.name());// 超出最大缓存if (cause == RemovalCause.SIZE) {}// 超出过期时间if (cause == RemovalCause.EXPIRED) {// do something}// 显式移除if (cause == RemovalCause.EXPLICIT) {// do something}// 旧数据被更新if (cause == RemovalCause.REPLACED) {// do something}}
}

redis消息监听:

@Slf4j
@Data
public class RedisCacheMessageListener implements MessageListener {private CaffeineCache caffeineCache;@Overridepublic void onMessage(Message message, byte[] pattern) {log.info("监听的redis message: {}" + message.toString());CacheMessage cacheMessage = JsonUtils.parseObject(message.toString(), CacheMessage.class);if (Objects.isNull(cacheMessage.getKey())) {caffeineCache.invalidate();} else {caffeineCache.evict(cacheMessage.getKey());}}
}

最后,通过自动配置类,注入相关bean:

*** @author fjzheng* @version 1.0* @date 2022/7/20 17:24*/
@Configuration
@EnableConfigurationProperties(MultilevelCacheProperties.class)
public class MultilevelCacheAutoConfiguration {@Resourceprivate MultilevelCacheProperties multilevelCacheProperties;ExecutorService cacheExecutor = new plasticeneThreadExecutor(Runtime.getRuntime().availableProcessors() * 2,Runtime.getRuntime().availableProcessors() * 20,Runtime.getRuntime().availableProcessors() * 200,"cache-pool");@Bean@ConditionalOnMissingBean({RedisTemplate.class})public  RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();template.setConnectionFactory(factory);template.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());template.setDefaultSerializer(new Jackson2JsonRedisSerializer<>(Object.class));template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));return template;}@Beanpublic RedisCache redisCache (RedisConnectionFactory redisConnectionFactory) {RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);RedisCacheConfiguration redisCacheConfiguration = defaultCacheConfig();redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.of(multilevelCacheProperties.getRedisExpireTime(), ChronoUnit.SECONDS));redisCacheConfiguration = redisCacheConfiguration.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));RedisCache redisCache = new CustomRedisCache(multilevelCacheProperties.getRedisName(), redisCacheWriter, redisCacheConfiguration);return redisCache;}/*** 由于Caffeine 不会再值过期后立即执行清除,而是在写入或者读取操作之后执行少量维护工作,或者在写入读取很少的情况下,偶尔执行清除操作。* 如果我们项目写入或者读取频率很高,那么不用担心。如果想入写入和读取操作频率较低,那么我们可以通过Cache.cleanUp()或者加scheduler去定时执行清除操作。* Scheduler可以迅速删除过期的元素,***Java 9 +***后的版本,可以通过Scheduler.systemScheduler(), 调用系统线程,达到定期清除的目的* @return*/@Bean@ConditionalOnClass(CaffeineCache.class)@ConditionalOnProperty(name = "multilevel.cache.caffeineSwitch", havingValue = "true", matchIfMissing = true)public CaffeineCache caffeineCache() {int maxCapacity = (int) (Runtime.getRuntime().totalMemory() * multilevelCacheProperties.getMaxCapacityRate());int initCapacity = (int) (maxCapacity * multilevelCacheProperties.getInitRate());CaffeineCache caffeineCache = new CaffeineCache(multilevelCacheProperties.getCaffeineName(), Caffeine.newBuilder()// 设置初始缓存大小.initialCapacity(initCapacity)// 设置最大缓存.maximumSize(maxCapacity)// 设置缓存线程池.executor(cacheExecutor)// 设置定时任务执行过期清除操作
//                .scheduler(Scheduler.systemScheduler())// 监听器(超出最大缓存).removalListener(new CaffeineCacheRemovalListener())// 设置缓存读时间的过期时间.expireAfterAccess(Duration.of(multilevelCacheProperties.getCaffeineExpireTime(), ChronoUnit.SECONDS))// 开启metrics监控.recordStats().build());return caffeineCache;}@Bean@ConditionalOnBean({CaffeineCache.class, RedisCache.class})public MultilevelCache multilevelCache(RedisCache redisCache, CaffeineCache caffeineCache) {MultilevelCache multilevelCache = new MultilevelCache(true, redisCache, caffeineCache);return multilevelCache;}@Beanpublic RedisCacheMessageListener redisCacheMessageListener(@Autowired CaffeineCache caffeineCache) {RedisCacheMessageListener redisCacheMessageListener = new RedisCacheMessageListener();redisCacheMessageListener.setCaffeineCache(caffeineCache);return redisCacheMessageListener;}@Beanpublic RedisMessageListenerContainer redisMessageListenerContainer(@Autowired RedisConnectionFactory redisConnectionFactory,@Autowired RedisCacheMessageListener redisCacheMessageListener) {RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);redisMessageListenerContainer.addMessageListener(redisCacheMessageListener, new ChannelTopic(multilevelCacheProperties.getTopic()));return redisMessageListenerContainer;}}

3.使用

使用非常简单,只需要通过multilevelCache操作即可:

@RestController
@RequestMapping("/api/data")
@Api(tags = "api数据")
@Slf4j
public class ApiDataController {@Resourceprivate MultilevelCache multilevelCache;@GetMapping("/put/cache")public void put() {DataSource ds = new DataSource();ds.setName("多级缓存");ds.setType(1);ds.setCreateTime(new Date());ds.setHost("127.0.0.1");multilevelCache.put("test-key", ds);}@GetMapping("/get/cache")public DataSource get() {DataSource dataSource = multilevelCache.get("test-key", DataSource.class);return dataSource;}}

4.总结

以上全部就是关于多级缓存的实现方案总结,多级缓存就是为了解决项目服务中单一缓存使用不足的缺点。应用场景有:接口权限校验,每次请求接口都需要根据当前登录人有哪些角色,角色有哪些权限,如果每次都去查数据库性能开销比较严重,再加上权限一般不怎么会频繁变更,所以使用多级缓存是最合适不过了;还有就是很多管理系统列表界面都有组织架构信息(所属部门、小组等),这些信息同样可以使用多级缓存来完美提升性能。

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

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

相关文章

纯函数 和 函数柯里化 05 (待补充)

加油&#xff0c;今天周二啦&#xff01;&#x1f60d; 文章目录 前言一、js 的纯函数二、副作用三、纯函数的优势四、JavaScript 柯里化五、柯里化作用 - 让函数的职责单一六、柯里化作用 - 逻辑的复用七、自动柯里化函数&#xff08;我没理解&#xff0c;等我理解了再更&…

如何在洛谷自己出的题中出数据

首先&#xff0c;假如你要加1个数据&#xff1a; 打开Dev-c&#xff08;其他也行&#xff09; 填入输入数据&#xff1a; &#xff08;这个数据只是我编的&#xff09; 将这个东东保存为in文件&#xff08;第一个数据就名为001&#xff0c;第二个002&#xff09;&#xff1a…

2022 robocom 世界机器人开发者大赛-本科组(国赛)

RC-u1 智能红绿灯 题目描述&#xff1a; RC-u1 智能红绿灯 为了最大化通行效率同时照顾老年人穿行马路&#xff0c;在某养老社区前&#xff0c;某科技公司设置了一个智能红绿灯。 这个红绿灯是这样设计的&#xff1a; 路的两旁设置了一个按钮&#xff0c;老年人希望通行马路时会…

没有进度管理的项目,都是在做无用功

在项目管理过程中&#xff0c;最大的挑战之一是确保项目实施与计划保持一致。 项目实施过程是一个相对漫长的过程&#xff0c;其中受到许多因素的影响。如果项目实施没有按照原始项目计划进行&#xff0c;很容易导致项目偏离计划&#xff0c;最终可能导致项目停滞或失败。 当…

Maven出现报错 ; Unable to import maven project: See logs for details错误的多种解决方法

问题现象; IDEA版本&#xff1a; Maven 版本 &#xff1a; 3.3.9 0.检查 maven 的设置 &#xff1a;F:\softeware\maven\apache-maven-3.9.3\conf 检查setting.xml 配置 本地仓库<localRepository>F:\softeware\maven\local\repository</localRepository>镜像…

使用 Python 和 Flask 构建简单的 Restful API 第 1 部分

一、说明 我将把这个系列分成 3 或 4 篇文章。在本系列的最后&#xff0c;您将了解使用flask构建 restful API 是多么容易。在本文中&#xff0c;我们将设置环境并创建将显示“Hello World”的终结点。 我假设你的电脑上安装了python 2.7和pip。我已经在python 2.7上测试了本文…

Activiti7

文章目录 概述入门案例1.创建springboot项目pom.xml 2.获取ProcessEngine2.1 默认的方式2.2 编程方式获取2.3 表结构介绍 3.在线流程设计器 概述 官网地址&#xff1a;https://www.activiti.org/ Activiti由Alfresco软件开发&#xff0c;目前最高版本Activiti 7。是BPMN的一个…

Flutter iOS 集成使用 flutter boost

在 Flutter项目中集成完 flutter boost&#xff0c;并且已经使用了 flutter boost进行了路由管理&#xff0c;这时如果需要和iOS混合开发&#xff0c;这时就要到 原生端进行集成。 注意&#xff1a;之前建的项目必须是 Flutter module项目&#xff0c;并且原生项目和flutter m…

bigemap如何添加高清在线地图?

说明&#xff1a;批量添加可以同时添加多个在线地图&#xff0c;一次性添加完成&#xff08;批量添加无法验证地址是否可以访问&#xff09; 添加后如下图&#xff1a; 第一步 &#xff1a; 制作地图配置文件&#xff1a;选择添加在线地图&#xff08;查看帮助&#xff09;。 …

Chatgpt AI newbing作画,文字生成图 BingImageCreator 二次开发,对接wxbot

开源项目 https://github.com/acheong08/BingImageCreator 获取cookie信息 cookieStore.get("_U").then(result > console.log(result.value)) pip3 install --upgrade BingImageCreator import os import BingImageCreatoros.environ["http_proxy"]…

基于 yolov8 的人体姿态评估

写在前面 工作中遇到&#xff0c;简单整理博文内容为使用预训练模型的一个预测 Demo测试图片来源与网络,如有侵权请告知理解不足小伙伴帮忙指正 对每个人而言&#xff0c;真正的职责只有一个&#xff1a;找到自我。然后在心中坚守其一生&#xff0c;全心全意&#xff0c;永不停…

华为认证 | 云计算HCIE3.0改版后有什么变化?

随着技术的不断进步和行业的发展&#xff0c;云计算的HCIE作为华为公司的顶级认证&#xff0c;也进行了版本的更新。 那改版后有哪些变化呢&#xff0c;今天给大家讲讲。 01 HCIE认证简介 HCIE认证是华为公司旗下的顶级专业认证&#xff0c;面向IT领域的高级专业人士。 它涵…

收集到大量的名片怎么转为excel?

来百度APP畅享高清图片 参加完展会或集体会议&#xff0c;是不是收了一大堆名片&#xff0c;保管起来超级麻烦&#xff0c;还容易丢三落四&#xff1f;别急&#xff0c;我们有办法&#xff01;把名片转成电子版保存到电脑上就完美啦&#xff01;但要是名片数量有点多&#xff0…

ChatGPT应用在AIGC内容生产【赠书活动|第一期《硅基物语》】

文章目录 爆火的AI工具ChatGPT走入大众视野的AIGCAIGC领域的发展AIGC价值引领『赠书活动 &#xff5c; 第一期』 爆火的AI工具ChatGPT 2023年伊始&#xff0c;ChatGPT就火遍全网&#xff0c;成为了全球最快拥有1亿月活用户的产品。在地铁上、电梯中、咖啡厅到处都充满着讨论AI…

五、PC远程控制ESP32 LED灯

1. 整体思路 2. 代码 # 整体流程 # 1. 链接wifi # 2. 启动网络功能(UDP) # 3. 接收网络数据 # 4. 处理接收的数据import socket import time import network import machinedef do_connect():wlan = network.WLAN(network.STA_IF)wlan.active(True)if not wlan.isconnected(…

【BI系统】选型常见问题解答二

本文主要总结BI系统选型过程中遇见的常见问题&#xff0c;并针对性做出回答&#xff0c;希望能为即将选型&#xff0c;或正在选型BI系统的企业用户们提供一个快速了解通道。 有针对金蝶云星空的BI方案吗&#xff1f;能起到怎样的作用&#xff1f; 答&#xff1a;奥威BI系统拥…

Nevron Vision for .NET Crack

Nevron Vision for .NET Crack NET Vision是一个用于创建具有数据可视化功能的强大数据表示应用程序的套件。该套件具有用于.NET的Nevron Chart、用于.NET的Nevron Diagram和用于.NET的Nevron User Interface。精心设计的对象模型、众多功能和高质量的演示使复杂数据的可视化变…

【MySQL】

这里写目录标题 MySQL架构一条sql执行流程MySQL数据存放电脑位置ibd文件结构行溢出是什么MySQL行记录存储格式 MySQL架构 MySQL 的架构共分为两层&#xff1a;Server 层和存储引擎层 Server层 Server 层主要负责建立连接、分析和执行 SQL。MySQL 里大多数的核心功能模块都在这实…

python机器学习(七)决策树(下) 特征工程、字典特征、文本特征、决策树算法API、可视化、解决回归问题

决策树算法 特征工程-特征提取 特征提取就是将任意数据转换为可用于机器学习的数字特征。计算机无法直接识别字符串&#xff0c;将字符串转换为机器可以读懂的数字特征&#xff0c;才能让计算机理解该字符串(特征)表达的意义。 主要分为&#xff1a;字典特征提取(特征离散化)…

Grafana V10 告警推送 邮件

最近项目建设完成&#xff0c;一个城域网项目&#xff0c;相关zabbix和grafana展示已经完&#xff0c;想了想&#xff0c;不想天天看平台去盯网络监控平台&#xff0c;索性对告警进行分类调整&#xff0c;增加告警的推送&#xff0c;和相关部门的提醒&#xff0c;其他部门看不懂…