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

news2024/12/26 20:14:15

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 {

    @Autowired
    private Cache cache;
    @Autowired
    private 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
     */
    @Bean
    public Cache<String, Object> localCache() {
        return Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .initialCapacity(100)
                .maximumSize(1000)
                .build();
    }

    /**
     * 获取redis本地客户端
     * @param lettuceConnectionFactory
     * @return
     */
    @Bean
    public RedisClient redisClient(@Autowired LettuceConnectionFactory lettuceConnectionFactory) {
        return (RedisClient) lettuceConnectionFactory.getNativeClient();
    }

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

注册缓存上下文(cacheFrontendContext)为bean对象----结合本地cache和redis客户端对象
/**
     * 缓存上下文
     * @param redisClient
     * @param cache
     * @return
     */
    @Bean
    public 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);
    @Getter
    private CacheFrontend cacheFrontend;
    private RedisClient redisClient;
    @Getter
    private 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;
    }

    @Override
    public Object get(Object key) {
        log.info("caffeine get => {}", key);
        return cache.getIfPresent(key);
    }

    @Override
    public void put(Object key, Object value) {
        log.info("caffeine set => {}:{}", key, value);
        cache.put(key, value);
    }

    @Override
    public 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方法
@SneakyThrows
    public 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;//五次尝试重连失败,就熔断降级
    }
}
@Bean
  public 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);
    @Getter
    private CacheFrontend cacheFrontend;
    private RedisClient redisClient;
    @Getter
    private Cache cache;
    @Qualifier("waitingSyncDataCache")
    private Cache waitingSyncDataCache;
    @Autowired
    private TryConnectTime tryConnectTime;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    StatefulRedisConnection<String, String> connection;

    public CacheFrontendContext(RedisClient redisClient, Cache cache) {
        this.redisClient = redisClient;
        this.cache = cache;
    }

    @SneakyThrows
    public 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.coloradmin.cn/o/2253328.html

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

相关文章

JavaScript实现tab栏切换

JavaScript实现tab栏切换 代码功能概述 这段代码实现了一个简单的选项卡&#xff08;Tab&#xff09;切换功能。它通过操作 HTML 元素的类名&#xff08;class&#xff09;来控制哪些选项卡&#xff08;Tab&#xff09;和对应的内容板块显示&#xff0c;哪些隐藏。基本思路是先…

【天地图】HTML页面实现车辆轨迹、起始点标记和轨迹打点的完整功能

目录 一、功能演示 二、完整代码 三、参考文档 一、功能演示 运行以后完整的效果如下&#xff1a; 点击开始&#xff0c;小车会沿着轨迹进行移动&#xff0c;点击轨迹点会显示经纬度和时间&#xff1a; 二、完整代码 废话不多说&#xff0c;直接给完整代码&#xff0c;替换…

HCIA笔记6--路由基础与静态路由:浮动路由、缺省路由、迭代查找

文章目录 0. 概念1.路由器工作原理2. 跨网访问流程3. 静态路由配置4. 静态路由的应用场景4.1 路由备份4.2 浮动路由4.3 缺省路由 5. 迭代路由6 问题6.1 为什么路由表中有的下一跳的地址有接口&#xff1f;6.2 个人电脑的网关本质是什么&#xff1f; 0. 概念 自治系统&#xff…

20241129解决在Ubuntu20.04下编译中科创达的CM6125的Android10出现找不到库文件libncurses.so.5的问题

20241129解决在Ubuntu20.04下编译中科创达的CM6125的Android10出现找不到库文件libncurses.so.5的问题 2024/11/29 21:11 缘起&#xff1a;中科创达的高通CM6125开发板的Android10的编译环境需要。 vendor/qcom/proprietary/commonsys/securemsm/seccamera/service/jni/jni_if.…

Matlab搜索路径添加不上

发现无论是右键文件夹添加到路径&#xff0c;还是在“设置路径”中专门添加&#xff0c;我的路径始终添加不上&#xff0c;导致代码运行始终报错&#xff0c;后来将路径中的“”加号去掉后&#xff0c;就添加成功了&#xff0c;经过测试&#xff0c;路径中含有中文也可以添加成…

自由学习记录(28)

C# 中的流&#xff08;Stream&#xff09; 流&#xff08;Stream&#xff09;是用于读取和写入数据的抽象基类。 流表示从数据源读取或向数据源写入数据的矢量过程。 C# 中的流类是从 System.IO.Stream 基类派生的&#xff0c;提供了多种具体实现&#xff0c;每种实现都针对…

Redis3——线程模型与数据结构

Redis3——线程模型与数据结构 本文讲述了redis的单线程模型和IO多线程工作原理&#xff0c;以及几个主要数据结构的实现。 1. Redis的单线程模型 redis6.0之前&#xff0c;一个redis进程只有一个io线程&#xff0c;通过reactor模式可以连接大量客户端&#xff1b;redis6.0为了…

使用playwright自动化测试时,npx playwright test --ui打开图形化界面时报错

使用playwright自动化测试时&#xff0c;npx playwright test --ui打开图形化界面时报错 1、错误描述&#xff1a;2、解决办法3、注意符号的转义 1、错误描述&#xff1a; 在运行playwright的自动化测试项目时&#xff0c;使用npm run test无头模式运行正常&#xff0c;但使用…

深度学习模型:门控循环单元(GRU)详解

本文深入探讨了门控循环单元&#xff08;GRU&#xff09;&#xff0c;它是一种简化版的长短期记忆网络&#xff08;LSTM&#xff09;&#xff0c;在处理序列数据方面表现出色。文章详细介绍了 GRU 的基本原理、与 LSTM 的对比、在不同领域的应用以及相关的代码实现&#xff0c;…

用html+jq实现元素的拖动效果——js基础积累

用htmljq实现元素的拖动效果 效果图如下&#xff1a; 将【item10】拖动到【item1】前面 直接上代码&#xff1a; html部分 <ul id"sortableList"><li id"item1" class"w1" draggable"true">Item 1</li><li …

单片机学习笔记 12. 定时/计数器_定时

更多单片机学习笔记&#xff1a;单片机学习笔记 1. 点亮一个LED灯单片机学习笔记 2. LED灯闪烁单片机学习笔记 3. LED灯流水灯单片机学习笔记 4. 蜂鸣器滴~滴~滴~单片机学习笔记 5. 数码管静态显示单片机学习笔记 6. 数码管动态显示单片机学习笔记 7. 独立键盘单片机学习笔记 8…

【乐企文件生成工程】搭建docker环境,使用docker部署工程

1、自行下载docker 2、自行下载docker-compose 3、编写Dockerfile文件 # 使用官方的 OpenJDK 8 镜像 FROM openjdk:8-jdk-alpine# 设置工作目录 WORKDIR ./app# 复制 JAR 文件到容器 COPY ../lq-invoice/target/lq-invoice.jar app.jar # 暴露应用程序监听的端口 EXPOSE 1001…

React基础知识三 router路由全指南

现在最新版本是Router6和Router5有比较大的变化&#xff0c;Router5和Router4变化不大&#xff0c;本文以Router6的写法为主&#xff0c;也会对比和Router5的不同。比较全面。 安装路由 npm i react-router-dom基本使用 有两种Router&#xff0c;BrowserRouter和HashRouter&…

【C#】书籍信息的添加、修改、查询、删除

文章目录 一、简介二、程序功能2.1 Book类属性&#xff1a;方法&#xff1a; 2.2 Program 类 三、方法&#xff1a;四、用户界面流程&#xff1a;五、程序代码六、运行效果 一、简介 简单的C#控制台应用程序&#xff0c;用于管理书籍信息。这个程序将允许用户添加、编辑、查看…

打造去中心化交易平台:公链交易所开发全解析

公链交易所&#xff08;Public Blockchain Exchange&#xff09;是指基于公有链&#xff08;如以太坊、波场、币安智能链等&#xff09;建立的去中心化交易平台。与传统的中心化交易所&#xff08;CEX&#xff09;不同&#xff0c;公链交易所基于区块链技术实现资产交换的去中心…

CLIP模型也能处理点云信息

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

关于NXP开源的MCU_boot的项目心得

MCU的启动流程细查 注意MCU上电第一个函数运行的就是Reset_Handler函数&#xff0c;下图是表示了这个函数做了啥事情&#xff0c;注意加强一下对RAM空间的段的印象&#xff0c;从上到下是栈&#xff0c;堆&#xff0c;.bss段&#xff0c;.data段。 bootloader的难点 固件完…

MySQL5.6升级MySQL5.7

升级方式介绍 08 数据库服务版本升级方法 5.6 – 5.7 – 8.0 数据库版本升级方法&#xff1a; Inplace-本地升级 步骤一&#xff1a;在同一台服务器中&#xff0c;需要部署高版本数据库服务实例步骤二&#xff1a;低版本数据库中的数据进行备份迁移&#xff0c;迁移到高版本…

怎么理解BeamSearch?

在大模型推理中&#xff0c;常会用到BeamSearch&#xff0c;本文就BeamSearch原理与应用理解展开讲解。 一、BeamSearch原理 Beam Search 是一种启发式搜索算法&#xff0c;常用于自然语言处理&#xff08;NLP&#xff09;和其他需要生成序列的任务中&#xff0c;比如机器翻译…

shodan2-批量查找CVE-2019-0708漏洞

声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&#…