本地缓存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的基本使用方法
- 导入依赖
- 构建公共缓存对象(expireAfterWrite方法可以设置写入后多久过期)
- 使用 put() 方法添加缓存
- 使用 getIfPresent() 方法读取缓存
- 一旦重启项目,缓存就都消失了(基于本地内存)!
四、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(二级缓存),可以构建高效的多级缓存解决方案。