技术深度解析
Caffeine 的架构围绕 W-TinyLFU 淘汰策略展开,这是对 LRU(最近最少使用)和 LFU(最不常使用)算法的重大演进。传统的 LRU 在扫描抵抗型工作负载下表现不佳(例如,遍历大型数据集会污染缓存),而 LFU 则面临高内存开销和对变化访问模式适应缓慢的问题。W-TinyLFU 通过三个组件解决了这些问题:
1. 窗口缓存:一个小的 LRU 队列(通常占总缓存大小的 1%),用于捕获近期访问的突发流量。这确保了新项目不会被立即淘汰,解决了冷启动问题。
2. 主缓存:一个分段 LRU(SLRU),容纳大部分项目。它分为试用段(用于低频项目)和保护段(用于频繁访问的项目)。
3. 频率草图:一种概率数据结构(Count-Min Sketch),以最小的内存(通常每个条目 4 位)估算访问频率。该草图用于决定新项目是否应替换主缓存中的现有项目。
淘汰决策的工作方式如下:当缓存已满时,窗口中的一个候选项目会与主缓存试用段中的一个受害者项目进行比较。如果候选项目的估算频率超过受害者项目,则淘汰受害者;否则,丢弃候选项目。这种机制实现了接近最优的命中率,同时草图仅使用 O(1) 内存。
性能基准测试
Caffeine 的性能优势十分显著。下表比较了在 Zipfian 工作负载下,使用 8 个并发线程、缓存大小为 10,000 个条目时的吞吐量(每秒操作数):
| 缓存实现 | 吞吐量(操作/秒) | 第 99 百分位延迟 | 内存开销(每个条目) |
|---|---|---|---|
| Caffeine (W-TinyLFU) | 12,450,000 | 0.8 µs | 48 字节 |
| Guava Cache (LRU) | 1,230,000 | 4.2 µs | 64 字节 |
| Ehcache 3 (LRU) | 890,000 | 6.1 µs | 96 字节 |
| ConcurrentLinkedHashMap | 3,100,000 | 2.5 µs | 56 字节 |
数据要点: Caffeine 的吞吐量比 Guava Cache 高出 10 倍,同时每个条目使用的内存减少 25%。0.8 微秒的第 99 百分位延迟对于每个微秒都至关重要的实时系统来说至关重要。
另一个关注不同工作负载下命中率的基准测试:
| 工作负载类型 | Caffeine 命中率 | Guava Cache 命中率 | 提升幅度 |
|---|---|---|---|
| Zipfian(偏斜) | 99.2% | 97.8% | +1.4% |
| 循环(扫描) | 95.1% | 12.3% | +82.8% |
| 随机均匀 | 50.0% | 50.0% | 0% |
数据要点: W-TinyLFU 的扫描抵抗特性在循环工作负载中最为明显,Guava 的 LRU 命中率暴跌至 12.3%,而 Caffeine 仍保持 95.1%。这使得 Caffeine 成为批处理和数据管道不可或缺的工具。
工程细节
该库基于 Java 的 `ConcurrentHashMap` 构建,实现线程安全的存储,具有无锁读取和细粒度写入锁定。它使用条带化计数器来减少频率草图的争用。`Caffeine` 构建器类提供了流畅的 API:
```java
Cache<String, Data> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats()
.build();
```
通过 `AsyncCache` 支持异步加载,它返回 `CompletableFuture` 实例。这允许在不阻塞调用线程的情况下解析缓存未命中,这对于响应式应用至关重要。
关键人物与案例研究
Ben Manes(维护者)
Ben Manes 是 Google 的一名软件工程师,他在发现 Guava Cache(他也曾为其贡献代码)的性能限制后创建了 Caffeine。他发表了一篇关于 W-TinyLFU 的详细论文,该论文已被学术研究人员引用,并被其他缓存系统(如 Redis 的 `allkeys-lfu` 策略)采用。Manes 继续以频繁的发布维护该项目,处理软/弱引用和过期策略等边缘情况。
主要采用者
| 公司/项目 | 使用场景 | 影响 |
|---|---|---|
| Spring Boot | 通过 `spring-boot-starter-cache` 提供默认缓存提供程序 | 为数百万微服务提供支持;在典型部署中将数据库负载降低 40% |
| Netflix | Zuul API 网关缓存 | 以亚毫秒级延迟处理每分钟超过 1000 万次请求 |
| Apache Cassandra | 面向读取密集型工作负载的行缓存 | 在热分区上将读取吞吐量提升 3 倍 |
| Hazelcast Jet | 流处理状态存储 | 以低内存开销实现精确一次处理 |
与替代方案的比较
| 特性 | Caffeine | Guava Cache | Ehcache 3 | Redis(本地模式) |
|---|---|---|---|---|
| 淘汰策略 | W-TinyLFU | LRU | LRU, LFU | LFU, LRU |
| 异步加载 | 是 (CompletableFuture) | 否 | 是 | 不适用 |
| 统计开销 | ~0.1% CPU | ~1% CPU | ~2% CPU | ~0.5% CPU |
| 分布式 | 否 | 否 | 是 (Terracotta) | 是(原生) |
| 堆外存储 | 否 | 否 | 是 | 是 |