本地缓存Caffeine详解(含与Spring Cache集成)

目录

一、介绍

二、Caffeine核心原理与架构设计

2.1 存储引擎与数据结构

2.2 缓存淘汰策略

2.3 并发控制机制

 三、入门案例

3.1 引入依赖

3.2 测试接口

3.3 小结

四、Caffeine常用方法详解

4.1 getIfPresent

4.2 get

4.3 put

4.4 putAll

4.5 invalidate

4.6 invalidateAll

五、构建一个更加全面的缓存

5.1、容量控制配置

(1)​​initialCapacity(int)​​⭐

(2)​​maximumSize(long)​​ ⭐

(3)​​maximumWeight(long)​​

5.2、过期策略配置

(1)expireAfterAccess(long, TimeUnit)​​

(2)​​expireAfterWrite(long, TimeUnit)​⭐​

​(3)​expireAfter(Expiry)​​

5.3 注意事项

六、整合Spring Cache

6.1 引入依赖

6.2 配置文件

6.3 使用

七、生产环境注意事项

八、实现Caffeine与Redis多级缓存完整策略(待完善)❗


一、介绍

JDK内置的Map可作为缓存的一种实现方式,然而严格意义来讲,其不能算作缓存的范畴。

原因如下:一是其存储的数据不能主动过期;二是无任何缓存淘汰策略。

Caffeine是一个基于Java 8的高性能本地缓存库,由Ben Manes开发,旨在提供快速、灵活的缓存解决方案。作为Guava Cache的现代替代品,Caffeine在性能、功能和灵活性方面都有显著提升。

Caffeine作为Spring体系中内置的缓存之一,Spring Cache同样提供调用接口支持。已成为Java生态中最受欢迎的本地缓存库之一。

本文将全面介绍Caffeine的核心原理、使用方法和最佳实践。

二、Caffeine核心原理与架构设计

2.1 存储引擎与数据结构

Caffeine底层采用优化的ConcurrentHashMap作为主要存储结构,并在此基础上进行了多项创新:

  • ​分段存储技术​​:使用StripedBuffer实现无锁化并发控制,将竞争分散到多个独立缓冲区,显著提升并发吞吐量。
  • ​频率统计机制​​:采用Count-Min Sketch算法记录访问频率,以93.75%的准确率仅使用少量内存空间。
  • ​时间轮管理​​:通过TimerWheel数据结构高效管理过期条目,实现纳秒级精度的过期控制。

2.2 缓存淘汰策略

Caffeine采用了创新的Window-TinyLFU算法,结合了LRU和LFU的优点:

  • ​三区设计​​:窗口区(20%)、试用区(1%)和主区(79%),各区使用LRU双端队列管理
  • ​动态调整​​:根据访问模式自动调整各区比例,最高可实现98%的缓存命中率
  • ​频率衰减​​:通过周期性衰减历史频率,防止旧热点数据长期占据缓存

相比Guava Cache的LRU算法,Window-TinyLFU能更准确地识别和保留真正的热点数据,避免"一次性访问"污染缓存。

2.3 并发控制机制

Caffeine的并发控制体系设计精妙:

  • ​写缓冲机制​​:使用RingBuffer和MpscChunkedArrayQueue实现多生产者-单消费者队列
  • ​乐观锁优化​​:通过ReadAndWriteCounterRef等自定义原子引用降低CAS开销
  • ​StampedLock应用​​:在关键路径上使用Java 8的StampedLock替代传统锁,提升并发性能

 三、入门案例

3.1 引入依赖

以springboot 2.3.x为例,

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

3.2 测试接口

package com.example.demo;import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.UUID;@RestController
@RequestMapping("/api")
public class Controller {@GetMapping("writeCache")public String writeCache() {Cache<Object, Object> cache = Caffeine.newBuilder().build();cache.put("uuid", UUID.randomUUID());User user = new User("张三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "写入缓存成功";}@GetMapping("readCache")public String readCache() {Cache<Object, Object> cache = Caffeine.newBuilder().build();Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}

 

问题:明明调用接口写入了缓存,为什么我们查询的时候还是没有呢?

细心的你可能已经发现了,我们在每个接口都重新构造了一个新的Cache实例。这两个Cache实例是完全独立的,数据不会自动共享。

解决办法

所以,聪明的你可能就想着把它提取出来,成功公共变量吧

@RestController
@RequestMapping("/api")
public class Controller {Cache<Object, Object> cache = Caffeine.newBuilder().build();@GetMapping("writeCache")public String writeCache() {cache.put("uuid", UUID.randomUUID());User user = new User("张三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "写入缓存成功";}@GetMapping("readCache")public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}

你看这不就有了!于是聪明的你,又想:“如果放在这个控制器类下面,那我其他类中要是想调用,是不是不太好?”

于是你又把它放在一个配置类下面,用于专门管理缓存。

package com.example.demo;import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class CacheConfig {@Beanpublic Cache<String, Object> buildCache() {return Caffeine.newBuilder().build();}
}
@RestController
@RequestMapping("/api")
public class Controller {@Resourceprivate Cache<String, Object> cache;@GetMapping("writeCache")public String writeCache() {cache.put("uuid", UUID.randomUUID());User user = new User("张三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "写入缓存成功";}@GetMapping("readCache")public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}

聪明的你,发现依然可以呀!真棒!

于是你又灵机一动,多定义几个bean吧,一个设置有效期,一个永不过期。

@Configuration
public class CacheConfig {@Bean("noLimit")public Cache<String, Object> buildCache() {return Caffeine.newBuilder().build();}@Bean("limited")public Cache<String, Object> buildLimitedCache() {// 设置过期时间是30sreturn Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.SECONDS).build();}
}
@RestController
@RequestMapping("/api")
public class Controller {@Resource(name = "limited")private Cache<String, Object> cache;@GetMapping("writeCache")public String writeCache() {cache.put("uuid", UUID.randomUUID());User user = new User("张三", "123456@qq.com", "abc123", 18);cache.put("user", user);return "写入缓存成功";}@GetMapping("readCache")public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.getIfPresent("user");return "uuid: " + uuid + ", user: " + user;}}

你发现30s后加入的缓存也没有了。

3.3 小结

通过这个案例,你似乎也觉察到了,Caffeine的基本使用方法

  1. 导入依赖
  2. 构建公共缓存对象(expireAfterWrite方法可以设置写入后多久过期)
  3. 使用 put() 方法添加缓存
  4. 使用 getIfPresent() 方法读取缓存
  5. 一旦重启项目,缓存就都消失了(基于本地内存)!

四、Caffeine常用方法详解

4.1 getIfPresent

@Nullable V getIfPresent(@CompatibleWith("K") @NonNull Object var1);

前面已经演示过了,这里就不在举例了。意思是如果存在则获取,不存在就是null。

4.2 get

@Nullable V get(@NonNull K var1, @NonNull Function<? super K, ? extends V> var2);

@GetMapping("readCache")
public String readCache() {Object uuid = cache.getIfPresent("uuid");Object user = cache.get("user", item -> {// 缓存不存在时,执行加载逻辑return new User("李四", "456789@qq.com", "def456", 20);});return "uuid: " + uuid + ", user: " + user;
}

4.3 put

 void put(@NonNull K var1, @NonNull V var2);

 入门案例也演示过了,就是添加缓存。使用方法和普通的map类似,都是key,value的形式。

4.4 putAll

void putAll(@NonNull Map<? extends @NonNull K, ? extends @NonNull V> var1);

putAll 顾名思义,就是可以批量写入缓存。首先定义一个map对象,把要加入的缓存往map里面塞,然后把map作为参数传递给这个方法即可。

4.5 invalidate

手动清除单个缓存

cache.invalidate("key1");

4.6 invalidateAll

手动批量清除多个key

// 批量清除多个key
cache.invalidateAll(Arrays.asList("key1", "key2"));

手动清除所有缓存

// 清除所有缓存
cache.invalidateAll();

💡注意:

这些方法会立即从缓存中移除指定的条目。

Caffeine除了手动清除外,也和Redis一样,有自动清除策略。这些将在下一张集中讲解。

五、构建一个更加全面的缓存

前面我们演示时,通过Caffeine.newBuilder().build();就建完了缓存对象,顶多给它设置了一个过期时间。

但是关于这个缓存对象本身,还有很多东西是可以设置的,下面我们就详细说说,还有哪些设置。

Caffeine.newBuilder() 提供了丰富的配置选项,可以创建高性能、灵活的缓存实例。以下是主要的可配置内容:

5.1、容量控制配置

(1)​​initialCapacity(int)​​⭐

设置初始缓存容量

示例:.initialCapacity(100) 表示初始能存储100个缓存对象

(2)​​maximumSize(long)​​ ⭐

按条目数量限制缓存大小

示例:.maximumSize(1000) 表示最多缓存1000个条目

(3)​​maximumWeight(long)​

按自定义权重总和限制缓存大小

需要配合weigher()使用

示例:.maximumWeight(10000).weigher((k,v) -> ((User)v).getSize())

注意:maximumSize和maximumWeight不能同时使用

当缓存条目数超过最大设定值时,Caffeine会根据Window TinyLFU算法自动清除"最不常用"的条目

5.2、过期策略配置

(1)expireAfterAccess(long, TimeUnit)​

设置最后访问后过期时间

示例:.expireAfterAccess(5, TimeUnit.MINUTES)

(2)​​expireAfterWrite(long, TimeUnit)​⭐​

设置创建/更新后过期时间

示例:.expireAfterWrite(10, TimeUnit.MINUTES)

​(3)​expireAfter(Expiry)​

自定义过期策略

可以基于创建、更新、读取事件分别设置

.expireAfter(new Expiry<String, Object>() {public long expireAfterCreate(String key, Object value, long currentTime) {return TimeUnit.HOURS.toNanos(1); // 创建1小时后过期}public long expireAfterUpdate(String key, Object value, long currentTime, long currentDuration) {return currentDuration; // 保持原过期时间}public long expireAfterRead(String key, Object value, long currentTime, long currentDuration) {return currentDuration; // 保持原过期时间}
})

5.3 注意事项

Caffeine的清除操作通常是异步执行的,如果需要立即清理所有过期条目,可以调用:

cache.cleanUp();

这个方法会触发一次完整的缓存清理,包括所有符合条件的过期条目。

六、整合Spring Cache

前面介绍时说了,Caffeine作为Spring体系中内置的缓存之一,Spring Cache同样提供调用接口支持。所以接下来,我们详细实现整合过程。

6.1 引入依赖

<!--  caffeine    -->
<dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId>
</dependency><!-- cache -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>

6.2 配置文件

@Configuration
public class CacheConfig {@Beanpublic CacheManager cacheManager() {CaffeineCacheManager cacheManager = new CaffeineCacheManager();cacheManager.setCaffeine(Caffeine.newBuilder().initialCapacity(100)          // 初始容量.maximumSize(500)             // 最大缓存条目数.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期.expireAfterAccess(5, TimeUnit.MINUTES) // 访问后5分钟过期.weakKeys()                   // 使用弱引用键.recordStats());              // 记录统计信息return cacheManager;}
}

6.3 使用

具体使用方法可以参考前面写的这篇文章Spring Cache用法很简单,但你知道这中间的坑吗?-CSDN博客

springcache无非就是那几个注解。这里浅浅举例演示

@RestController
@RequestMapping("/api")
public class Controller {@GetMapping("test")@Cacheable(value = "demo")public User test() {System.out.println("-----------------------");return new User("张三", "123456@qq.com", "abc123", 18);}}


多次刷新,idea控制台也仅仅打印了一次---------------------------

说明缓存生效了!

七、生产环境注意事项

提到缓存,那就是老生常谈的:缓存穿透、缓存击穿和缓存雪崩等问题。

缓存穿透防护​​:

  • 对null值进行适当缓存(使用unless = "#result == null"
  • 考虑使用Bloom过滤器

​缓存雪崩防护​​:

  • 为不同缓存设置不同的过期时间
  • 添加随机抖动因子到过期时间

​缓存一致性​​:

  • 重要数据建议配合数据库事务
  • 考虑使用@CachePut更新策略

​内存管理​​:

  • 合理设置maximumSize防止OOM
  • 对大对象考虑使用weakValues()softValues()

​分布式环境​​:

  • 本地缓存需要配合消息总线实现多节点同步
  • 或考虑使用多级缓存(本地+Redis)

八、实现Caffeine与Redis多级缓存完整策略(待完善)❗

在现代高并发系统中,多级缓存架构已成为提升系统性能的关键手段。Spring Cache通过抽象缓存接口,结合Caffeine(一级缓存)和Redis(二级缓存),可以构建高效的多级缓存解决方案。

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

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

相关文章

论特定领域软件架构

20250413-作 题目 特定领域软件架构DSSA&#xff08;Domain Specific Software Architecture&#xff09;就是在一个特定应用领域中为一组应用提供组织结构参考的标准软件体系结构。对DSSA 研究的角度、关心的问题不同导致了对DSSA 的不同定义。DSSA 的必备特征如下。 一…

iOS 远程调试与离线排查实战:构建非现场问题复现机制

iOS开发者都知道&#xff0c;调试最怕两个字&#xff1a;“偶发”。用户说App闪退了&#xff0c;你点了十遍也没问题&#xff1b;测试说功能卡顿了&#xff0c;你抓日志时它又顺滑如新。最麻烦的是&#xff0c;这种“现场问题”往往在你连接不到用户设备时发生。 面对这种情况…

SpringBoot -- 整合 Swagger3

8.SpringBoot3 整合 Swagger3 由于目前主流的开发模式是前后端分离开发。所以前后端的交互需要通过一个 API&#xff08;开发接口&#xff09; 来规范。而这个接口的开发是由后端程序员编写的。“网站式 API 文档” 1.导入依赖 <!-- 引入swagger3(springdoc)的依赖--> …

A Machine Learning Approach for Non-blind Image Deconvolution论文阅读

A Machine Learning Approach for Non-blind Image Deconvolution 1. 研究目标与实际意义2. 创新方法与模型设计2.1 核心思路2.2 正则化反演:理论与公式2.2.1 退化模型2.2.2 正则化目标函数2.2.3 傅里叶域闭式解2.3 MLP去伪影:架构与训练2.3.1 MLP架构设计2.3.2 训练流程2.3.…

微信小程序<rich-text>支持里面图片点击放大

使用<rich-text>渲染类似下面的html代码&#xff1a; <div stylecolor: red>宠物友好<br/>xxx提供宠物友好服务&#xff0c;具体请见下图<br/></div> <img srchttps://xxx.com/xxx1.png width100%/> <img srchttps://xxx.com/xxx2.png…

BVH 文件是一种用于记录 3D 动画数据的文件格式,常用于 3D 建模和动画制作。以下是对这个 BVH 文件的逐行解读

BVH 文件是一种用于记录 3D 动画数据的文件格式&#xff0c;常用于 3D 建模和动画制作。以下是对这个 BVH 文件的逐行解读&#xff1a; HIERARCHY ROOT Hips { OFFSET 0 0 0 CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation JOINT LeftUpLeg { OFFSE…

C语言开发:Onvif(一)

根据ONVIF官网 的介绍&#xff1a; ONVIF是一个开放的安防行业组织&#xff0c;致力于为安防行业提供和促进标准化开放接口&#xff0c;以实现IP网络安防产品和服务的有效互操作性。 在具体实现上&#xff0c;ONVIF使用了Web Service的方式&#xff0c;设备通过WSDL定义的接口…

中科米堆三维扫描仪耳机3D扫描尺寸测量数字化建模

当下&#xff0c;耳机已从单纯的音频输出设备进化为集娱乐、健康、办公于一体的智能穿戴终端。相关数据显示&#xff0c;2025年全球智能个人音频设备出货量突破4.55亿台&#xff0c;中国以22%的增速领跑全球&#xff0c;其中开放式耳机&#xff08;OWS&#xff09;出货量占比达…

学习接口自动化框架pytest有哪些好处?

学习 pytest 作为接口自动化测试框架&#xff0c;具有以下显著优势&#xff0c;能大幅提升测试效率和质量&#xff1a; Pytest自动化测试教程&#xff0c;自动化必备之Pytest测试框架训练营&#xff0c;只需一小时速成&#xff0c;学会直接上手实操&#xff01; 1. 代码简洁&…

LinuxBridge的作用与发展历程:从基础桥接到云原生网络基石

LinuxBridge作为Linux网络虚拟化的核心组件&#xff0c;已悄然支撑互联网基础设施20余年。它不仅是虚拟机网络连接的桥梁&#xff0c;更是容器网络、云原生架构的隐形支柱。 一、LinuxBridge的核心作用剖析 1. 二层网络虚拟化的实现者 本质功能&#xff1a;模拟物理交换机行为…

探讨 SYSCFG 时钟对 ADC 采样的影响

探讨 SYSCFG 时钟对 ADC 采样的影响 在嵌入式系统开发中&#xff0c;对于 GD32F4 系列微控制器&#xff0c;理解 SYSCFG 时钟与 ADC 采样之间的关系至关重要。 以下是详细分析&#xff1a; 一、SYSCFG 时钟对 ADC 采样过程的基础性作用 1. 提供采样时钟信号 ADC 采样需要时钟…

Spring Boot 项目实训 - 图书信息网站

文章目录 1. 实训目的2. 系统设计3. 运行演示4. 实现步骤4.1 创建数据库与表4.2 创建Spring Boot项目4.3 创建实体类4.4 创建映射器接口4.5 编写应用配置文件4.6 测试映射器接口4.7 创建服务类4.8 创建控制器4.9 准备静态资源4.10 创建模板页面4.11 测试整个项目 5. 实训总结 1…

GPU 性能可变性分析框架

大家读完觉得有帮助记得关注和点赞&#xff01;&#xff01;&#xff01; 抽象。 分析来自 GPU 分析器的大规模性能日志通常需要数 TB 的内存和数小时的运行时间&#xff0c;即使是基本摘要也是如此。这些限制会阻止及时洞察&#xff0c;并阻碍将性能分析集成到自动化工作流程中…

Redis主从复制原理

一、Redis主从复制概述 Redis主从复制&#xff08;Master-Slave Replication&#xff09;是Redis实现高可用性和读写分离的基础架构。通过主从复制&#xff0c;数据可以从一个Redis服务器&#xff08;主节点&#xff09;复制到一个或多个Redis服务器&#xff08;从节点&#x…

将ONNX模型转换为(OPENMV可用的格式)TensorFlow Lite格式

将ONNX模型转换为TensorFlow Lite格式 在深度学习模型部署过程中&#xff0c;我们常常需要将模型从一种格式转换为另一种格式&#xff0c;以适应不同的硬件平台和应用场景。本文将详细介绍如何将ONNX格式的模型转换为TensorFlow Lite格式&#xff0c;以便在移动设备或嵌入式设…

EXILIUM×亚矩云手机:重构Web3虚拟生存法则,开启多端跨链元宇宙自由征途

在链游与元宇宙赛道竞争加剧的当下&#xff0c;EXILIUM凭借其去中心化开放世界、链上资产确权与玩家自治经济系统&#xff0c;成为Web3原住民逃离“中心化牢笼”的首选之地。然而&#xff0c;其多链交互门槛高、跨设备身份管理复杂、链上安全风险频发等问题&#xff0c;让普通玩…

用3个字符表示2字节二进制数据

把1字节二进制数据&#xff0c;用可打印字符表示&#xff0c;可用十六进制。这样一来&#xff0c;1字节变2字节&#xff0c;2倍。 2字节的二进制数据&#xff0c;可以用3个字符来表示&#xff0c;1.5倍。 2562略小于413&#xff0c;65536<68921 需要准备41个符号 包括&…

第 3 章:神经网络如何学习

第 3 章&#xff1a;神经网络如何学习 在第二章中&#xff0c;我们详细了解了神经网络的静态结构&#xff1a;由神经元组成的层&#xff0c;以及连接它们的权重和偏置。现在&#xff0c;我们将进入整个教程最核心的部分&#xff1a;神经网络是如何从数据中"学习"的&…

RocketMQ 消息长轮询

文章目录 问题所在&#xff1a;消费者如何高效地获取消息&#xff1f;解决方案&#xff1a;长轮询 (Long Polling - “等待与观察”模式)长轮询 vs. 短轮询&#xff08;可视化对比&#xff09;为什么这个机制对 RocketMQ 这么好&#xff1f;关键的配置参数 让我们用一个简单易懂…

TensorFlow Serving学习笔记3: 组件调用关系

一、整体架构 TensorFlow Serving 采用模块化设计&#xff0c;核心组件包括&#xff1a; Servables&#xff1a;可服务对象&#xff08;如模型、查找表&#xff09;Managers&#xff1a;管理 Servable 生命周期&#xff08;加载/卸载&#xff09;Loaders&#xff1a;负责 Ser…