目录
一、介绍
二、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(二级缓存),可以构建高效的多级缓存解决方案。