单机环境下Caffeine和Redis两级缓存的实现与问题解决

1. Caffeine和Redis介绍

Caffeine 是一个高性能、基于 Java 的缓存库。它被设计用来作为 Java 应用程序的本地缓存解决方案,提供了比传统缓存更高效的功能和性能。Caffeine 可以缓存任意类型的数据,并具有丰富的配置选项,以满足不同应用的缓存需求。

Caffeine 通过使用多种缓存策略(如基于大小、时间、引用等),支持自动过期、最大容量限制、以及基于异步加载的缓存操作。它的设计目标是高效、低延迟,并能在多线程环境中保持性能。

Redis 是一个开源的、基于内存的数据结构存储系统,支持多种数据结构,如字符串(string)、哈希(hash)、列表(list)、集合(set)、有序集合(sorted set)等。它不仅可以用作数据库,还可以作为缓存和消息中间件。由于其速度非常快(大多数操作在内存中进行),Redis 被广泛应用于需要快速读取和写入的场景,例如缓存、会话管理、实时数据分析、消息队列等。

在本文章中,我们就详细介绍一下,如何使用Caffeine和Redis搭建一个本地+远程的二级缓存结构并且让它们的交互变得更加丝滑。

2. 搭建过程

2.1 SpringBoot 项目引入依赖

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>3.1.8</version></dependency>

2.2 注册Caffeine中cache对象为Bean对象

@Bean
public Cache<String, Object> localCache() {return Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).initialCapacity(100).maximumSize(1000).build();}

后续使用只需要注入这个类型为Cache的bean对象就可以调用其中的增删查改方法.

基本使用: 

cache.put("key1","value1");//存入key-value键值对
cache.getIfPresent("key1");//根据key删除键值对
cache.invalidate("key1");//根据key删除键值对

2.3 注入操作Redis客户端的对象stringRedisTemplate

@Autowired
private StringRedisTemplate stringRedisTemplate;

基本使用:

stringRedisTemplate.opsForValue().set("key1", "value1");//存入key-value键值对
stringRedisTemplate.opsForValue().get("key1");//根据key获取键值对
stringRedisTemplate.opsForValue().del("key1");//根据key删除键值对

2.4 缓存工具类CacheUtil结合使用

我们给出get方法作为示例:

@Configuration
public class CacheUtil {@Autowiredprivate Cache cache;@Autowiredprivate StringRedisTemplate stringRedisTemplate;public boolean set(String key, String value) {String localCache = cache.getIfPresent("key1";//Caffeine本地缓存获取key-value键值对String remoteCache = stringRedisTemplate.opsForValue().get("key1");//Redis远程缓存获取key-value键值对return true;} catch (Exception e) {log.error("CacheUtil error, set({}, {})", key, value, e, tryConnectTime.isDegrade());return false;}}
}

3. 进阶问题

3.1 如何让两者产生关联?

我们上述的例子实际上仅仅是把这两个缓存的get相关代码"放到了一起",实际上并没有让这两者产生任何关联性.思考一下,在Redis中查询不到数据的时候,我们是不是会查询数据库,再把对应数据放入Redis中.那么,我们实际上可以使用"桥接模式",仍然让Caffeine中的缓存数据从Redis中获取.

注册bean对象----本地cache和redis客户端
/*** 配置Caffeine本地缓存* @return*/@Beanpublic Cache<String, Object> localCache() {return Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).initialCapacity(100).maximumSize(1000).build();}/*** 获取redis本地客户端* @param lettuceConnectionFactory* @return*/@Beanpublic RedisClient redisClient(@Autowired LettuceConnectionFactory lettuceConnectionFactory) {return (RedisClient) lettuceConnectionFactory.getNativeClient();}

其中redis客户端的获取使用了"工厂模式",直接使用注入的工厂即可.

注册缓存上下文(cacheFrontendContext)为bean对象----结合本地cache和redis客户端对象
/*** 缓存上下文* @param redisClient* @param cache* @return*/@Beanpublic CacheFrontendContext cacheFrontendContext(@Autowired RedisClient redisClient, @Autowired Cache cache) {return new CacheFrontendContext(redisClient, cache);}

这个对象可以用来帮助我们设置Caffeine跟踪Redis中的数据变化.

public class CacheFrontendContext {private static final Logger log = LoggerFactory.getLogger(CacheFrontendContext.class);@Getterprivate CacheFrontend cacheFrontend;private RedisClient redisClient;@Getterprivate Cache cache;StatefulRedisConnection<String, String> connection;public CacheFrontendContext(RedisClient redisClient, Cache cache) {this.redisClient = redisClient;this.cache = cache;}
}
核心追踪方法
connection.addListener(message -> {List<Object> content = message.getContent(StringCodec.UTF8::decodeKey);log.info("type:{}, content:{}",message.getType(), content);if (message.getType().equalsIgnoreCase("invalidate")) {List<String> keys = (List<String>) content.get(1);keys.forEach(key -> {cache.invalidate(key);});}
});

当Redis中数据变动后,当我们尝试从Caffeine中根据某个key值获取对应value时,会监听消息的类型,如果类型为"invalidate"(已经变动),就自动清除Caffeine中这对key-value缓存,再重新将Redis中的数据放入Caffeine.

Caffeine-Redis桥接关键
connection = redisClient.connect();CacheFrontend<String, String> frontend = ClientSideCaching.enable(new CaffeineCacheAccessor(cache),connection,TrackingArgs.Builder.enabled());
this.cacheFrontend = frontend;

通过这个代码片段设置对Redis消息的追踪.

CaffeineCacheAccessor----Caffeine-Redis桥接类

这个类的是十分必要的,Caffeine和Redis本身是毫不相干的两个组件,要将它们结合在一起,除了追踪以外,仍然需要告诉Caffeine:获取到Redis中的最新数据后,应该怎么处理这些数据,再存放到Caffeine中.

public class CaffeineCacheAccessor implements CacheAccessor {private static final Logger log = LoggerFactory.getLogger(CaffeineCacheAccessor.class);private Cache cache;public CaffeineCacheAccessor(Cache cache) {this.cache = cache;}@Overridepublic Object get(Object key) {log.info("caffeine get => {}", key);return cache.getIfPresent(key);}@Overridepublic void put(Object key, Object value) {log.info("caffeine set => {}:{}", key, value);cache.put(key, value);}@Overridepublic void evict(Object key) {log.info("caffeine evict => {}", key);cache.invalidate(key);}
}

通过这样一系列的设置,我们就能够把get部分的代码简化:

@Autowired
private CacheFrontendContext cacheFrontendContext;public String get(String key) {return cacheFrontendContext.getCacheFrontend().get(key).toString();
}

在存数据时只需要将数据存入Redis中即可,在读取时优先读取Caffeine中的数据,如果不存在或者过期了,Caffeine会自动从Redis中读取数据,成功让Caffeine和Redis产生了依赖关系.

3.2 Redis挂了怎么办?(熔断降级与断开重连的实现)

经过上述的操作,Caffeine的确和Redis产生了依赖关系,但是如果Redis挂了怎么办?我们就不能再向Redis中存数据了.那么我们就需要实现熔断降级,就是解开这些组件的耦合关系.在这个案例中,就是实现Caffeine本地缓存的存取数据,不至于影响到整个系统.那么我们实际上可以构造一个check方法轮询来确保Redis的连接状态,如果连接断开了我们就尝试重新连接,如果多次重新连接失败,就利用Caffeine来存取数据.

check方法
@SneakyThrowspublic void check() {if (connection != null) {if (connection.isOpen()) {  return;}}try {tryConnectTime.increase();//成功降级就减少重连的频率if (tryConnectTime.isDegrade()) Thread.sleep(5000);//重新建立连接connection = redisClient.connect();CacheFrontend<String, String> frontend = ClientSideCaching.enable(new CaffeineCacheAccessor(cache),connection,TrackingArgs.Builder.enabled());this.cacheFrontend = frontend;//添加监听,如果redis数据变动,caffeine获取时自动清除缓存connection.addListener(message -> {List<Object> content = message.getContent(StringCodec.UTF8::decodeKey);log.info("type:{}, content:{}",message.getType(), content);if (message.getType().equalsIgnoreCase("invalidate")) {List<String> keys = (List<String>) content.get(1);keys.forEach(key -> {cache.invalidate(key);});}});log.warn("The redis client side connection had been reconnected.");}catch (RuntimeException e) {log.error("The redis client side connection had been disconnected, waiting reconnect...", e);}}
TryConnectTime----用于计重连次数的类
public class TryConnectTime {private volatile AtomicInteger time = new AtomicInteger(0);public void increase() {time.incrementAndGet();}public void reset() {time.set(0);}public boolean isDegrade() {return time.get() > 5;//五次尝试重连失败,就熔断降级}
}
@Beanpublic TryConnectTime tryConnectTime() {return new TryConnectTime();
}

CacheUtil工具类set方法示例:

public boolean set(String key, String value) {try {if (tryConnectTime.isDegrade()) {return setByCaffeine(key, value);}return setByRedis(key, value);} catch (Exception e) {log.error("CacheUtil error, set({}, {}), isDegrade:{}", key, value, e, tryConnectTime.isDegrade());return false;}
}

3.3 Redis重连后的数据一致性问题

那么我们之前说Caffeine是依赖于Redis中的数据的,如果Redis重启后,在这段Redis挂掉期间的缓存数据是存放在Caffeine中的,当Redis服务又可用时会清除它自己的所有缓存数据,会不会把Caffeine中实际有用的数据当作过期的数据,从而进行覆盖呢?实际上是有可能的.

解决方法也十分简单,我们只需要记录下这段期间(熔断降级后到Redis服务可用)内对Caffeine缓存数据的变动,另外设置一个Caffeine的bean对象,把这些数据在Redis重新成功连接时,再设置回到Redis中.(因为有两个Cache类型的bean对象,需要使用@Qualifier根据名称注入,@Autowired默认是根据类型注入)

@Bean
public Cache<String, Object> waitingSyncDataCache() {return Caffeine.newBuilder().expireAfterWrite(120, TimeUnit.MINUTES).initialCapacity(100).maximumSize(3000).build();
}

check完整方法

public class CacheFrontendContext {private static final Logger log = LoggerFactory.getLogger(CacheFrontendContext.class);@Getterprivate CacheFrontend cacheFrontend;private RedisClient redisClient;@Getterprivate Cache cache;@Qualifier("waitingSyncDataCache")private Cache waitingSyncDataCache;@Autowiredprivate TryConnectTime tryConnectTime;@Autowiredprivate StringRedisTemplate stringRedisTemplate;StatefulRedisConnection<String, String> connection;public CacheFrontendContext(RedisClient redisClient, Cache cache) {this.redisClient = redisClient;this.cache = cache;}@SneakyThrowspublic void check() {if (connection != null) {if (connection.isOpen()) {if (!waitingSyncDataCache.asMap().entrySet().isEmpty()) {syncDataToRedis(waitingSyncDataCache);}tryConnectTime.reset();return;}}try {tryConnectTime.increase();if (tryConnectTime.isDegrade()) Thread.sleep(5000);//重新建立连接connection = redisClient.connect();CacheFrontend<String, String> frontend = ClientSideCaching.enable(new CaffeineCacheAccessor(cache),connection,TrackingArgs.Builder.enabled());this.cacheFrontend = frontend;if (!waitingSyncDataCache.asMap().entrySet().isEmpty()) {syncDataToRedis(waitingSyncDataCache);}//添加监听,如果redis数据变动,caffeine自动清除缓存connection.addListener(message -> {List<Object> content = message.getContent(StringCodec.UTF8::decodeKey);log.info("type:{}, content:{}",message.getType(), content);if (message.getType().equalsIgnoreCase("invalidate")) {List<String> keys = (List<String>) content.get(1);keys.forEach(key -> {cache.invalidate(key);});}});log.warn("The redis client side connection had been reconnected.");}catch (RuntimeException e) {log.error("The redis client side connection had been disconnected, waiting reconnect...", e);}}private void syncDataToRedis(Cache waitingSyncDataCache) {Set<Map.Entry<String, String>> entrySet = waitingSyncDataCache.asMap().entrySet();for (Map.Entry<String, String> entry : entrySet) {if (!stringRedisTemplate.hasKey(entry.getKey())) {stringRedisTemplate.opsForValue().set(entry.getKey(), entry.getValue());log.info("同步key:{},value:{}到Redis客户端",entry.getKey(), entry.getValue());}waitingSyncDataCache.invalidate(entry.getKey());}}
}

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

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

相关文章

<数据集>路面坑洼识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;665张 标注数量(xml文件个数)&#xff1a;665 标注数量(txt文件个数)&#xff1a;665 标注类别数&#xff1a;1 标注类别名称&#xff1a;[pothole] 序号类别名称图片数框数1pothole6651740 使用标注工具&#x…

Oracle篇—通过官网下载最新的数据库软件或者历史数据库软件

&#x1f4ab;《博主介绍》&#xff1a;✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ &#x1f4ab;《擅长领域》&#xff1a;✌️擅长Oracle、MySQL、SQLserver、阿里云AnalyticDB for MySQL(分布式数据仓库)、Linux&#xff0c;也在扩展大数据方向的知识面✌️…

dns实验3:主从同步-完全区域传输

服务器192.168.234.111&#xff08;主服务器&#xff09;&#xff0c;打开配置文件&#xff1a; 打开配置文件&#xff1a; 关闭防火墙&#xff0c;改宽松模式&#xff1a; 重启服务&#xff1a; 服务器192.168.234.112&#xff08;从服务器&#xff09;&#xff0c;打开配置文…

OpenCV圆形标定板检测算法findCirclesGrid原理详解

OpenCV的findCirclesGrid函数检测圆形标定板的流程如下:   findCirclesGrid函数源码: //_image,输入图像 //patternSize,pattern的宽高 //_centers,blobs中心点的位置 //flags,pattern是否对称 //blobDetector,这里使用的是SimpleBlobDetector bool cv::findCirclesGrid(…

Java - JSR223规范解读_在JVM上实现多语言支持

文章目录 1. 概述2. 核心目标3. 支持的脚本语言4. 主要接口5. 脚本引擎的使用执行JavaScript脚本执行groovy脚本1. Groovy简介2. Groovy脚本示例3. 如何在Java中集成 Groovy4. 集成注意事项 6. 与Java集成7. 常见应用场景8. 优缺点9. 总结 1. 概述 JSR223&#xff08;Java Spe…

自然语言处理:基于BERT预训练模型的中文命名实体识别(使用PyTorch)

命名实体识别&#xff08;NER&#xff09; 命名实体识别&#xff08;Named Entity Recognition, NER&#xff09;是自然语言处理&#xff08;NLP&#xff09;中的一个关键任务&#xff0c;其目标是从文本中识别出具有特定意义的实体&#xff0c;并将其分类到预定义的类别中。这…

【C++】数组

1.概述 所谓数组&#xff0c;就是一个集合&#xff0c;该集合里面存放了相同类型的数据元素。 数组特点&#xff1a; &#xff08;1&#xff09;数组中的每个数据元素都是相同的数据类型。 &#xff08;2&#xff09;数组是有连续的内存空间组成的。 2、一维数组 2.1维数组定…

微软表示不会使用你的 Word、Excel 数据进行 AI 训练

​微软否认使用 Microsoft 365 应用程序&#xff08;包括 Word、Excel 和 PowerPoint&#xff09;收集数据来训练公司人工智能 (AI) 模型的说法。 此前&#xff0c;Tumblr 的一篇博文声称&#xff0c;雷德蒙德使用“互联体验”功能抓取客户的 Word 和 Excel 数据&#xff0c;用…

leetcode--螺旋矩阵

LCR 146.螺旋遍历二维数组 给定一个二维数组 array&#xff0c;请返回「螺旋遍历」该数组的结果。 螺旋遍历&#xff1a;从左上角开始&#xff0c;按照 向右、向下、向左、向上 的顺序 依次 提取元素&#xff0c;然后再进入内部一层重复相同的步骤&#xff0c;直到提取完所有元…

C++ 分治

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 1.分治法 2.二分搜索 函数传参——数组 3.棋盘覆盖 4.合并排序 5.快速排序 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 1.分治法 基…

Spark常问面试题---项目总结

一、数据清洗&#xff0c;你都清洗什么&#xff1f;或者说 ETL 你是怎么做的&#xff1f; 我在这个项目主要清洗的式日志数据&#xff0c;日志数据传过来的json格式 去除掉无用的字段&#xff0c;过滤掉json格式不正确的脏数据 过滤清洗掉日志中缺少关键字段的数据&#xff…

基于智能语音交互的智能呼叫中心工作机制

在智能化和信息化不断进步的现代&#xff0c;智能呼叫中心为客户提供高质量、高效率的服务体验&#xff0c;提升众多品牌用户的满意度和忠诚度。作为实现智能呼叫中心的关键技术之一的智能语音交互技术&#xff0c;它通过集成自然语言处理&#xff08;NLP&#xff09;、语音识别…

Android Studio 右侧工具栏 Gradle 不显示 Task 列表

问题&#xff1a; android studio 4.2.1版本更新以后AS右侧工具栏Gradle Task列表不显示&#xff0c;这里需要手动去设置 解决办法&#xff1a; android studio 2024.2.1 Patch 2版本以前的版本设置&#xff1a;依次打开 File -> Settings -> Experimental 选项&#x…

SpringBoot集成Kafka和avro和Schema注册表

Schema注册表 为了提升kafka的性能&#xff0c;减少网络传输和存储的数据大小&#xff0c;可以把数据的schema部分单独存储到外部的schema注册表中&#xff0c;整体架构如下图所示&#xff1a; 1&#xff09;把所有数据需要用到的 schema 保存在注册表里&#xff0c;然后在记…

http(请求方法,状态码,Cookie与)

目录 1.http中常见的Header(KV结构) 2.http请求方法 2.1 请求方法 2.2 telnet 2.3 网页根目录 2.3.1 概念 2.3.2 构建一个首页 2.4 GET与POST方法 2.4.1 提交参数 2.4.2 GET与POST提交参数对比 2.4.3 GET和POST对比 3.状态码 3.1 状态码分类 3.2 3XXX状态码 3.2 …

十,[极客大挑战 2019]Secret File1

点击进入靶场 查看源代码 有个显眼的紫色文件夹&#xff0c;点击 点击secret看看 既然这样&#xff0c;那就回去查看源代码吧 好像没什么用 抓个包 得到一个文件名 404 如果包含"../"、"tp"、"input"或"data"&#xff0c;则输出"…

pytest自定义命令行参数

实际使用场景&#xff1a;pytest运行用例的时候&#xff0c;启动mitmdump进程试试抓包&#xff0c;pytest命令行启动的时候&#xff0c;传入mitmdump需要的参数&#xff08;1&#xff09;抓包生成的文件地址 &#xff08;2&#xff09;mitm的proxy设置 # 在pytest的固定文件中…

Unity AssetBundles(AB包)

目录 前言 AB包是什么 AB包有什么作用 1.相对Resources下的资源AB包更好管理资源 2.减小包体大小 3.热更新 官方提供的打包工具:Asset Bundle Browser AB包资源加载 AB包资源管理模块代码 前言 在现代游戏开发中&#xff0c;资源管理是一项至关重要的任务。随着游戏内容…

(一)Linux下安装NVIDIA驱动(操作记录)

目录 一、查看CUDA版本 1.输入nvidia-smi&#xff0c;查看驱动支持的最大CUDA版本&#xff0c;这里是11.6 2.输入nvcc --version&#xff0c;查看当前安装的CUDA版本&#xff0c;这里是11.3 二、卸载旧的NVIDIA驱动 1.卸载原有驱动 2.禁用nouveau&#xff08;必须&#x…

用Python做数据分析环境搭建及工具使用(Jupyter)

目录 一、Anaconda下载、安装 二、Jupyter 打开 三、Jupyter 常用快捷键 3.1 创建控制台 3.2 命令行模式下的快捷键 3.3 运行模式下快捷键 3.4 代码模式和笔记模式 3.5 编写Python代码 一、Anaconda下载、安装 【最新最全】Anaconda安装python环境_anaconda配置python…