当前位置: 首页 > news >正文

本地缓存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.lqws.cn/news/548569.html

相关文章:

  • Java 工程智能化升级:飞算科技重构软件开发的技术范式
  • 电子电气架构 --- 涵盖“诊断与 ECU 平台”领域特有项目要求(上)
  • go写前端打包的自动化工具
  • 图像分割模型中的空间信息、上下文信息、空间路径、上下文路径到底是什么?有什么作用?
  • 大事件项目记录5-用户接口开发-更新用户头像
  • 未来已来:Deepoc大模型驱动的人机智能革命
  • ELK监控jar
  • 电商数据开发实践:深度剖析1688商品详情 API 的技术与应用
  • java中对象可达性分析 + 自动回收算法
  • Linux基本指令篇 —— tac指令
  • 导出docker-compse.yml中docker镜像成tar文件
  • 麒麟系统使用-运用VSCode运行.NET工程
  • swift 对象转Json
  • 分布式系统ID生成方案深度解析:雪花算法 vs UUID vs 其他主流方案
  • Hyperledger Fabric 入门笔记(二十)Fabric V2.5 测试网络进阶之Tape性能测试
  • Ubuntu 20.04 系统上运行 SLAM卡顿是什么原因
  • 免安装一键修复网络诊断 + 权限修复!打印机共享错误工具适配 Win7/10/11
  • Spring Boot 项目实训 - 图书信息网站
  • 移动端测试——如何解决iOS端无法打开弹窗式网页(Webkit)
  • canvas面试题200道
  • C++:string类(1)
  • 临床项目计划框架
  • java代码规范
  • 机器学习2——贝叶斯理论下
  • 【Linux手册】进程终止:进程退出和信号的响应机制
  • 微软全新开源的Agentic Web网络项目:NLWeb详解
  • 【C/C++】单元测试实战:Stub与Mock框架解析
  • 【世纪龙科技】吉利博瑞汽车车身诊断与校正仿真教学软件
  • window显示驱动开发—DirectX 图形内核子系统(二)
  • Ai大模型 - ocr图像识别形成结构化数据(pp-ocr+nlp结合) 以及训练微调实现方案(初稿)