Redis+Caffeine构造多级缓存

news2025/5/17 8:36:00

一、背景

项目中对性能要求极高,因此使用多级缓存,最终方案决定是Redis+Caffeine。其中Redis作为二级缓存,Caffeine作为一级本地缓存。

二、Caffeine简单介绍

Caffeine是一款基于Java 8的高性能、灵活的本地缓存库。它提供了近乎最佳的命中率,低延迟的读写操作,并且支持多种缓存策略,号称本地缓存之王。

核心特性

  • Caffeine的底层数据存储采用ConcurrentHashMap。因为Caffeine面向JDK8,在jdk8中ConcurrentHashMap增加了红黑树,在hash冲突严重时也能有良好的读性能。
  • Caffeine采用了先进的缓存淘汰算法,如Window TinyLfu,以提供极高的缓存命中率和低延迟的读写操作。
  • Caffeine支持多种缓存策略,包括过期时间、容量限制和引用权重等。用户可以根据实际需求,为不同的缓存对象设置合适的策略,以优化缓存性能。
  • Caffeine内部采用了细粒度的锁机制(ConcurrentHashMap),保证了缓存的线程安全。用户无需担心并发访问导致的缓存一致性问题。
  • Caffeine允许用户为缓存对象添加监听器,以便在缓存事件发生时(如创建、更新、删除等)执行自定义逻辑。

清除策略

Caffeine提供了三种缓存驱逐策略:

  • 基于容量:设置缓存的数量上限。
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(1) // 设置缓存大小上限为 1
    .build();
  • 基于时间:设置缓存的有效时间
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
    .expireAfterWrite(Duration.ofSeconds(10)) 
    .build();
  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
    // 构建cache对象
        Cache<String, String> cache = Caffeine.newBuilder()
                .weakKeys().weakValues().build();

Caffeine.weakKeys() 使用弱引用存储key。如果没有强引用这个key,则GC时允许回收该条目
Caffeine.weakValues() 使用弱引用存储value。如果没有强引用这个value,则GC时允许回收该条目
Caffeine.softValues() 使用软引用存储value, 如果没有强引用这个value,则GC内存不足时允许回收该条目

在这里插入图片描述

三、Window TinyLfu算法浅析

最让人吃惊的是,Caffeine的性能甚至超越了java中自身的map等内存。这是因为它采用的算法Window TinyLfu提供了一个近乎最佳的命中率,在此简单介绍一下Caffeine采用的核心算法,不想看的也可以跳过这部分。

W-TinyLFU(Window Tiny Least Frequently Used)算法是对传统LFU算法的优化与增强。

算法流程如下:当一个新数据进入时,首先会经过筛选比较,进入W-LRU窗口队列。这一设计旨在应对流量突增的情况。经过W-LRU窗口队列的筛选后,数据会进入过滤器。在过滤器中,算法会根据数据的访问频率来判断是否应将其加入缓存。若某个数据的最近访问次数较低,则被视为在未来被访问的可能性也较低。当缓存空间不足时,这些访问频率低的数据将优先被淘汰。

W-TinyLFU的优点在于:

  • 它使用Count-Min Sketch算法来存储访问频率,这种算法极大地节省了存储空间。
  • 通过定期衰减操作,算法能够灵活应对访问模式的变化。
  • W-LRU机制有助于避免缓存污染,确保高频访问的数据得以保留。
  • 过滤器内部的筛选处理能够有效防止低频数据替换高频数据。

然而,W-TinyLFU也存在一些局限性。它是由谷歌工程师发明的算法,目前主要应用于Caffeine Cache组件,应用范围相对有限。

关于Count-Min Sketch算法,它可视为与布隆过滤器具有同源性的算法。传统上,使用hashmap来存储每个元素的访问次数可能会导致较大的存储开销,并且在hash冲突时需要进行额外处理以避免数据误差。而Count-Min Sketch算法通过多个hash操作降低了hash冲突的概率。当获取元素频率时,该算法会找到多个索引位置,并取其中的最低值作为元素的频率,即Count Min的含义所在。

下图展示了Count-Min Sketch算法简单的工作原理:

  • 假设有四个hash函数。每当元素被访问时,其对应的计数会加1。
  • 算法会根据这四个hash函数计算元素的位置,并在相应位置进行加1操作。
  • 当需要获取元素的访问频率时,同样通过hash计算找到四个索引位置,并获取这些位置的频率信息。
  • 最后,根据Count Min原则,选择这四个频率中的最低值作为元素的最终频率值返回。
    在这里插入图片描述

三、实际应用

首先展示一下总体的思路流程图:
img
此篇文章不涉及到多容器下的本地Caffeine缓存同步的问题,后续会在本篇文章基础上写同步相关的处理手段。目前来看,大部分系统的本地缓存都不需要同步。

1、首先引入pom依赖

<!-- Caffeine -->
		<dependency>
			<groupId>com.github.ben-manes.caffeine</groupId>
			<artifactId>caffeine</artifactId>
		</dependency>

2、封装Caffeine相关的API

因为caffeine的api比较杂乱,为了统一管理和方便使用,我们需要对caffeine常用的api进行封装处理。
创建一个interface接口用作封装,代码如下:

/**
 * @description: Caffeine封装接口
 * @author: chenggh
 * @date: 2024/3/22
 */
public interface CaffeineCache<K, V> {

    /**
     * put
     *
     * @param key
     * @param value
     */
    void put(K key, V value);

    /**
     * get
     *
     * @param key
     * @return
     */
    V get(K key);

    /**
     * 判断是否包含K
     *
     * @param key
     * @return
     */
    boolean containsKey(K key);

    /**
     * 判断是否包含V
     *
     * @param value
     * @return
     */
    boolean containsValue(V value);

    /**
     * 移除某个K
     *
     * @param key
     */
    void remove(Object key);

    /**
     * 查询缓存命中,驱逐等数量
     *
     * @return
     */
    CacheStats cacheStats();

    /**
     * 清除全部(性能较慢,考虑场景使用)
     */
    void clear();

    /**
     * 转成MAP
     *
     * @return
     */
    ConcurrentMap<K, V> asMap();

    /**
     * 获取values
     *
     * @return
     */
    Collection<V> values();

    /**
     * 获取缓存大小
     *
     * @return
     */
    long size();

    /**
     * 主动回收已失效的缓存
     *
     * @return
     */
    void cleanUp();

    /**
     * 当缓存中有这个key就使用key对应的value值 如果没有就使用默认的value
     *
     * @return
     */
    V getOrDefault(K k, V v);

    /**
     * entrySet
     *
     * @return
     */
    Set<Map.Entry<K, V>> entrySet();
}

3、编写Caffeine封装API实现类,并将初始化的过程抽象进去。

  • maximumSize:最大容量,超过会自动清理。
  • removalListener:监听器,当缓存对象发生变更时会被监听到,key,value ==> 键值对 cause ==> 清理原因。
  • expireAfterWrite:全局时间淘汰策略,此处设置最后一次写入或访问后经过固定时间过期

其余参数说明:
initialCapacity 初始的缓存空间大小
maximumSize 缓存的最大条数
maximumWeight 缓存的最大权重
expireAfterAccess 最后一次写入或访问后,经过固定时间过期
expireAfterWrite 最后一次写入后,经过固定时间过期
refreshAfterWrite 写入后,经过固定时间过期,下次访问返回旧值并触发刷新
weakKeys 打开 key 的弱引用
weakValues 打开 value 的弱引用
softValues 打开 value 的软引用
recordStats 缓存使用统计
expireAfterWrite 和 expireAfterAccess 同时存在时,以expireAfterWrite 为准。
weakValues 和 softValues 不可以同时使用。
maximumSize 和 maximumWeight 不可以同时使用。

/**
 * @description: Caffeine封装API实现类
 * @author: chenggh
 * @date: 2024/3/22
 */
public class CaffeineCacheLocal<K, V> implements CaffeineCache<K, V> {
    private final Cache<K, V> localCache;

    private RemovalListener<? super K, ? super V> removalListener;
    private long maximumSize = -1L;
    private long duration = -1L;
    private TimeUnit unit;

    public CaffeineCacheLocal() {
        localCache = initCache();
    }

    public CaffeineCacheLocal(RemovalListener<? super K, ? super V> removalListener, long maximumSize, long duration, TimeUnit unit) {
        if (removalListener != null) {
            this.removalListener = removalListener;
        }
        if (unit != null) {
            this.unit = unit;
        }
        this.duration = duration;
        this.maximumSize = maximumSize;
        this.localCache = initCache();
    }

    /**
     * 初始化
     *
     * @return
     */
    private Cache<K, V> initCache() {
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder();
        //暂时未加入权重逻辑 所以maximumSize必须设定
        //若加入权重逻辑后,可以根据是否有权重判断处理
        if (this.maximumSize <= 0L) {
            throw new RuntimeException("maximumSize is must be set");
        }

        //key的最大条数
        caffeine.maximumSize(this.maximumSize);

        //expireAfterWrite全局时间淘汰策略,此处设置最后一次写入或访问后经过固定时间过期
        if (this.duration > 0L && this.unit != null) {
            caffeine.expireAfterWrite(this.duration, this.unit);
        }

        //开启淘汰监听
        if (this.removalListener != null) {
            caffeine.removalListener(this.removalListener);
        }
        // 初始的缓存空间大小,可以不设置
        //caffeine.initialCapacity(100);
        return caffeine.build();
    }

    @Override
    public void put(K key, final V value) {
        localCache.put(key, value);
    }

    @Override
    public V get(K key) {
        if (Objects.nonNull(key)){
            return localCache.getIfPresent(key);
        }
        return null;
    }

    @Override
    public boolean containsKey(K key) {
        return asMap().containsKey(key);
    }

    @Override
    public boolean containsValue(V value) {
        return asMap().containsValue(value);
    }

    @Override
    public void remove(Object key) {
        localCache.invalidate(key);
    }

    @Override
    public CacheStats cacheStats() {
        return localCache.stats();
    }

    @Override
    public void clear() {
        localCache.invalidateAll();
    }

    @Override
    public ConcurrentMap<K, V> asMap() {
        return localCache.asMap();
    }

    @Override
    public Collection<V> values() {
        return asMap().values();
    }

    @Override
    public long size() {
        return localCache.estimatedSize();
    }

    @Override
    public void cleanUp() {
        localCache.cleanUp();
    }

    @Override
    public V getOrDefault(K k, V defaultValue) {
        V v;
        return ((v = get(k)) != null) ? v : defaultValue;
    }

    @Override
    public Set<Map.Entry<K, V>> entrySet() {
        return asMap().entrySet();
    }

    public static Builder<Object, Object> newBuilder() {
        return new Builder<>();
    }

    public static class Builder<K1, V1> {
        private RemovalListener<? super K1, ? super V1> removalListener;
        private long maximumSize;
        private long duration;
        private TimeUnit unit;

        public Builder<K1, V1> removalListener(RemovalListener removalListener) {
            this.removalListener = removalListener;
            return this;
        }

        public Builder<K1, V1> maximumSize(long maximumSize) {
            this.maximumSize = maximumSize;
            return this;
        }

        public Builder<K1, V1> expireAfterWrite(long duration, TimeUnit unit) {
            this.duration = duration;
            this.unit = unit;
            return this;
        }

        public <K extends K1, V extends V1> CaffeineCache<K, V> build() {
            return new CaffeineCacheLocal<>(removalListener, maximumSize, duration, unit);
        }
    }
}

4、编写自定义配置类,方便IOC容器注入使用实现类对象

在这里我定义了两个一级缓存的bean对象,因为我们前边已经封装好了api方法和初始化配置,可以根据自己需求进行定义和注入。

@Configuration
public class CacheConfig {

    private static Logger log = LoggerFactory.getLogger(CacheConfig.class);

    @Bean
    public CaffeineCache<String, String> localStringCache() {
        return CaffeineCacheLocal.newBuilder()
                .maximumSize(800)
                .expireAfterWrite(2, TimeUnit.MINUTES)
                .removalListener((key, value, cause) -> {
                    /*log.info("[移除缓存] key:{} reason:{}", key, cause.name());
                    if (cause == RemovalCause.SIZE) {
                        log.info("超出最大缓存");
                    }
                    if (cause == RemovalCause.EXPIRED) {
                        log.info("超出过期时间");
                    }
                    if (cause == RemovalCause.EXPLICIT) {
                        log.info("显式移除");
                    }
                    if (cause == RemovalCause.REPLACED) {
                        log.info("旧数据被更新");
                    }*/
                })
                .build();
    }


    @Bean
    public CaffeineCache<Long, Map<String,String>> localMapCache() {
        return CaffeineCacheLocal.newBuilder()
                .maximumSize(50)
                .expireAfterWrite(1, TimeUnit.MINUTES)
                .removalListener((key, value, cause) -> {
                    // log.info("移除了Map-key:" + key + "  value:" + value + " cause:" + cause);
                })
                .build();
    }
}

5、使用

  • 查询
/**
     * 接口调用频率限制查询
     * @param key
     * @return
     */
    public RateLimitRule getRateLimitRule(String key) {
        key = key + ":limitation";
        String rl = localStringCache.get(key);
        if(!StringUtils.isEmpty(rl)) {
            log.info("走一级缓存");
            return JSON.parseObject(rl, RateLimitRule.class);
        }
        rl = redisTemplate.opsForValue().get(key);
        if(!StringUtils.isEmpty(rl)) {
            localStringCache.put(key, rl);
            log.info("走二级redis缓存");
            return JSON.parseObject(rl, RateLimitRule.class);
        }
        return null;
    }
  • 修改
/**
     * 设置、修改接口调用频率限制
     * @param rule
     * rule.getApiKey() = application+":"+methodType+":"+uri 远程服务会拼接好此参数进行传递
     * @return
     */
    public ResultDto updateRateLimitRule(RateLimitRule rule) {
        ResultDto resultDto = new ResultDto();
        resultDto.setCode(ResultCodeEnum.SUCCESS.getCode());
        String key = rule.getApiKey() + ":limitation";
        String jsonString = JSON.toJSONString(rule);
        redisTemplate.opsForValue().set(key, jsonString);
        localStringCache.put(key, jsonString);
        // 发布缓存更新消息 --同步到全部容器中,此步在后边做本地缓存同步时说明
        caffeineCacheUpdateSubscriber.publishUpdateMessage(key, jsonString, "localStringCache", CommonConstant.REPLACED_TYPE);
        return resultDto;
    }
  • 删除
/**
     * 删除接口调用频率限制
     * @param key
     * @return
     */
    public Boolean delRateLimitRule(String key) {
        String key_limit = key + ":limitation";
        redisTemplate.delete(key_limit);
        localStringCache.remove(key_limit);
        // 发布缓存删除消息 --同步到全部容器中,此步在后边做本地缓存同步时说明
        caffeineCacheUpdateSubscriber.publishUpdateMessage(key_limit, null, "localStringCache", CommonConstant.DELETE_TYPE);
        return true;
    }

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

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

相关文章

docker(四)使用篇二:docker 镜像

在上一章中&#xff0c;我们介绍了 docker 镜像仓库&#xff0c;本文就来介绍 docker 镜像。 一、什么是镜像 docker 镜像本质上是一个 read-only 只读文件&#xff0c; 这个文件包含了文件系统、源码、库文件、依赖、工具等一些运行 application 所必须的文件。 我们可以把…

AXI4总线协议 ------ AXI_LITE协议

一、AXI 相关知识介绍 https://download.csdn.net/download/mvpkuku/90841873 AXI_LITE 选出部分重点&#xff0c;详细文档见上面链接。 1.AXI4 协议类型 2.握手机制 二、AXI_LITE 协议的实现 1. AXI_LITE 通道及各通道端口功能介绍 2.实现思路及框架 2.1 总体框架 2.2 …

Ubuntu24.04 安装 5080显卡驱动以及cuda

前言 之前使用Ubuntu22.04版本一直报错,然后换了24.04版本才能正常安装 一. 配置基础环境 Linux系统进行环境开发环境配置-CSDN博客 二. 安装显卡驱动 1.安装驱动 按以下步骤来&#xff1a; sudo apt update && sudo apt upgrade -y#下载最新内核并安装 sudo add…

SpringAI-RC1正式发布:移除千帆大模型!

续 Spring AI M8 版本之后&#xff08;5.1 发布&#xff09;&#xff0c;前几日 Spring AI 悄悄的发布了最新版 Spring AI 1.0.0 RC1&#xff08;5.13 发布&#xff09;&#xff0c;此版本也将是 GA&#xff08;Generally Available&#xff0c;正式版&#xff09;发布前的最后…

操作系统之进程和线程听课笔记

计算机的上电运行就是构建进程树,进程调度就是在进程树节点进程进行切换 进程间通信的好处 经典模型 生产者和消费者 进程和线程的区别 线程引入带来的问题线程的优势 由于unix70年代产生,90年代有线程,当时数据库系统操作需要线程,操作系统没有来得及重造,出现了用户态线…

COMSOL随机参数化表面流体流动模拟

基于粗糙度表面的裂隙流研究对于理解地下水的流动、污染物传输以及与之相关的地质灾害&#xff08;如滑坡&#xff09;等方面具有重要意义。本研究通过蒙特卡洛方法生成随机表面形貌&#xff0c;并利用COMSOL Multiphysics对随机参数化表面的微尺度流体流动进行模拟。 参数化…

JavaSwing中的容器之--JScrollPane

JavaSwing中的容器之–JScrollPane 在Java Swing中&#xff0c;容器是用于容纳其他组件&#xff08;如按钮、标签等&#xff09;的组件。Swing提供了多种容器&#xff0c;它们可以嵌套使用以创建复杂的用户界面。 JScrollPane是一个轻量级组件&#xff0c;提供可滚动视图。JSc…

使用 Cookie 实现认证跳转功能

使用 Cookie 实现认证跳转功能的实践与解析 在 Web 开发中&#xff0c;用户身份认证是一个基础而关键的功能点。本文将通过一个简单的前后端示例系统&#xff0c;介绍如何基于 Cookie 实现 Token 保存与自动跳转认证的功能&#xff0c;并结合 Cookie 与 Header 的区别、使用场…

LED接口设计

一个LED灯有3种控制状态&#xff0c;常亮、常灭和闪烁&#xff0c;要做到这种控制最简单的一种方法是使用任何一款处理器的普通IO去控制。 用IO控制方式有两种&#xff0c;一种是高有效&#xff0c;如下图1所示IO口为高电平时LED亮&#xff0c;IO为低电平时LED不亮。IO口出一个…

SpringBoot项目使用POI-TL动态生成Word文档

近期项目工作需要动态生成Word文档的需求&#xff0c;特意调研了动态生成Word的技术方案。主要有以下两种&#xff1a; 第一种是FreeMarker模板来进行填充&#xff1b;第二种是POI-TL技术使用Word模板来进行填充&#xff1b; 以下是关于POI-TL的官方介绍 重点关注&#xff1…

YOLOv3深度解析:多尺度特征融合与实时检测的里程碑

一、YOLOv3的诞生&#xff1a;继承与突破的起点 YOLOv3作为YOLO系列的第三代算法&#xff0c;于2018年由Joseph Redmon等人提出。它在YOLOv2的基础上&#xff0c;针对小目标检测精度低、多类别标签预测受限等问题进行了系统性改进。通过引入多尺度特征图检测、残差网络架构和独…

uniapp-商城-60-后台 新增商品(属性的选中和页面显示)

前面添加了属性&#xff0c;添加属性的子级项目。也分析了如何回显&#xff0c;但是在添加新的商品的时&#xff0c;我们也同样需要进行选择&#xff0c;还要能正常的显示在界面上。下面对页面的显示进行分析。 1、界面情况回顾 属性显示其实是个一嵌套的数据显示。 2、选中的…

虹科技术 | 简化汽车零部件测试:LIN/CAN总线设备的按键触发功能实现

汽车零部件测试领域对操作的便捷性要求越来越高&#xff0c;虹科Baby-LIN-RC系列产品为这一需求提供了完美的解决方案。从基础的按键设置到高级的Shift键应用&#xff0c;本文将一步步引导您了解虹科Baby-LIN-RC系列产品的智能控制之道。 虹科Baby-LIN-3-RC 想象一下&#xff0…

单片机ESP32天气日历闹铃语音播报

自制Arduino Esp32 单片机 可以整点语音播报&#xff0c;闹铃语音播报&#xff0c;农历显示&#xff0c;白天晚上天气&#xff0c;硬件有 Esp32&#xff0c;ST7789显示屏&#xff0c;Max98357 喇叭驱动&#xff0c;小喇叭一枚。有需要源码的私信我。#单片机 #闹钟 #嵌入式 #智能…

如何解决LCMS 液质联用液相进样器定量环漏液问题

以下是解决安捷伦1260液相色谱仪为例的进样器定量环漏液问题的一些方法&#xff1a;视频操作 检查相关部件 检查定量环本身&#xff1a;观察定量环是否有破损、裂纹或变形等情况。如果发现定量环损坏&#xff0c;需及时更换。检查密封垫&#xff1a;查看进样阀的转子密封垫、计…

服务器内部可以访问外部网络,docker内部无法访问外部网络,只能docker内部访问

要通过 iptables 将容器中的特定端口请求转发到特定服务器&#xff0c;你需要设置 DNAT&#xff08;目标地址转换&#xff09;规则。以下是详细步骤&#xff1a; 假设场景 容器端口: 8080&#xff08;容器内服务监听的端口&#xff09;目标服务器: 192.168.1.100&#xff08;请…

PCIe Switch 问题点

系列文章目录 文章目录 系列文章目录完善PCIe Retimer Overview Document OutlineSwitch 维度BroadComMicroChipAsmedia 祥硕Cyan其他 完善 Functional block diagram&#xff0c;功能框图Key Features and Benefits&#xff0c;主要功能和优点Fabric 链路Multi-root PCIe Re…

开源轻量级地图解决方案leaflet

Leaflet 地图&#xff1a;开源轻量级地图解决方案 Leaflet 是一个开源的 JavaScript 库&#xff0c;用于在网页中嵌入交互式地图。它以轻量级、灵活性和易用性著称&#xff0c;适用于需要快速集成地图功能的项目。以下是关于 Leaflet 的详细介绍和使用指南。 1. Leaflet 的核心…

Flutter目录结构介绍、入口、Widget、Center组件、Text组件、MaterialApp组件、Scaffold组件

目录 1. 创建Flutter项目 1.1使用Android Studio创建Flutter项目 1.2 使用命令行创建Flutter项目 2. Flutter项目介绍 2.1所有代码都在lib目录下编写 2.1 pubspec.yaml 依赖库/图片的引用 ​编辑 3. 运行项目 4. 编写mian.dart文件 4.1 使用MaterialApp 和 Scaffold两个组件…

如何实现金蝶云星空到MySQL的数据高效集成

金蝶云星空数据集成到MySQL的技术案例分享 在企业信息化建设中&#xff0c;数据的高效流动和准确处理是关键。本文将聚焦于一个具体的系统对接集成案例&#xff1a;金蝶云星空的数据集成到MySQL&#xff0c;方案名称为“xsck-2金蝶销售出库-->mysql”。通过这一案例&#x…