Caffeine 核心源码总览
Caffeine 核心源码总览
本文面向 caffeine/src/main 的源码阅读,重点说明核心设计、模块职责和主要运行流程。源码包主体是 com.github.benmanes.caffeine.cache,它实现了同步、加载、异步、异步加载四类本地内存缓存,并通过构建器按需组合淘汰、过期、引用、刷新、统计和监听能力。
目录入口
| 文件 | 角色 |
|---|---|
Caffeine.java |
用户入口和构建器。校验配置,并根据是否有边界能力创建 bounded 或 unbounded cache。 |
Cache.java, LoadingCache.java, AsyncCache.java, AsyncLoadingCache.java |
对外 API。 |
LocalManualCache.java, LocalLoadingCache.java, LocalAsyncCache.java, LocalAsyncLoadingCache.java |
API 的骨架实现,把用户调用转发到底层 LocalCache。 |
LocalCache.java |
内部统一抽象,扩展 ConcurrentMap,补充统计、刷新、过期、引用和维护接口。 |
UnboundedLocalCache.java |
无自动淘汰/过期的轻量实现,本质是 ConcurrentHashMap 包装。 |
BoundedLocalCache.java |
有边界能力的核心实现,支持 Window TinyLFU、过期、弱/软引用、异步维护等。 |
LocalCacheFactory.java |
根据 builder 特性选择代码生成的 BoundedLocalCache 子类。 |
Node.java, NodeFactory.java |
缓存条目抽象和节点工厂;实际节点类按特性代码生成。 |
FrequencySketch.java |
TinyLFU 的 4-bit Count-Min Sketch 频率估计器。 |
TimerWheel.java |
可变过期时间的层级时间轮。 |
AccessOrderDeque.java, WriteOrderDeque.java, LinkedDeque.java |
淘汰和过期队列。 |
Buffer.java, BoundedBuffer.java, StripedBuffer.java, MpscGrowableArrayQueue.java |
读写缓冲,降低并发路径上的锁竞争。 |
Policy.java |
对外暴露的低层策略视图,例如 eviction、expiration、refresh。 |
stats/* |
统计计数器和快照。 |
构建与实现选择
Caffeine.newBuilder() 创建 builder,用户通过链式方法配置容量、权重、过期、引用强度、刷新、监听器、统计、执行器等。最终的 build* 方法有四个方向:
build()返回Cachebuild(CacheLoader)返回LoadingCachebuildAsync()返回AsyncCachebuildAsync(AsyncCacheLoader)返回AsyncLoadingCache
选择逻辑很直接:
- 没有大小/时间/引用等边界能力时,使用
UnboundedLocalCache。 - 有任一边界能力,或 loading cache 开启
refreshAfterWrite时,使用BoundedLocalCache。 - 异步 cache 的底层值类型是
CompletableFuture<V>,同步视图会在读取时解包已完成 future。
flowchart TD
A[用户调用 Caffeine.newBuilder] --> B[配置容量/过期/引用/统计/监听/执行器]
B --> C{调用 build 类型}
C --> D[Cache]
C --> E[LoadingCache]
C --> F[AsyncCache]
C --> G[AsyncLoadingCache]
D --> H{isBounded?}
E --> I{isBounded 或 refreshAfterWrite?}
F --> J{isBounded?}
G --> K{isBounded 或 refreshAfterWrite?}
H -- 否 --> L[UnboundedLocalManualCache]
I -- 否 --> M[UnboundedLocalLoadingCache]
J -- 否 --> N[UnboundedLocalAsyncCache]
K -- 否 --> O[UnboundedLocalAsyncLoadingCache]
H -- 是 --> P[BoundedLocalManualCache]
I -- 是 --> Q[BoundedLocalLoadingCache]
J -- 是 --> R[BoundedLocalAsyncCache]
K -- 是 --> S[BoundedLocalAsyncLoadingCache]
代码生成模型
BoundedLocalCache 是抽象类,具体子类不是手写在 src/main 中,而是由 caffeine/src/javaPoet 生成:
LocalCacheFactory.getClassName(builder)根据配置拼出类名,例如 key/value 强度、监听器、统计、最大容量、访问过期、写入过期、刷新等。NodeFactory.getClassName(builder, isAsync)根据节点需要的字段拼出节点类名,例如 key/value 引用强度、access/write/variable 时间、refresh、maximum、weight 等。- Gradle 任务
generateLocalCaches和generateNodes将代码生成到build/generated/sources/local-cache与build/generated/sources/node。
这个设计的目标是避免在运行时为每个配置都携带无用字段和分支,让常见路径更紧凑。
flowchart LR
A[Caffeine builder 配置] --> B[LocalCacheFactory.getClassName]
A --> C[NodeFactory.getClassName]
B --> D[加载生成的 BoundedLocalCache 子类]
C --> E[加载生成的 NodeFactory/Node 类]
D --> F[运行时 cache 实例]
E --> F
核心数据结构
BoundedLocalCache 的核心状态可以分为五组:
ConcurrentHashMap<Object, Node<K,V>> data:主索引,key 可能是强引用 key,也可能是弱引用包装。Node:保存 key、value、weight、accessTime、writeTime、variableTime、队列指针和生命周期状态。- 策略队列:Window、Main Probation、Main Protected 三段访问队列,以及 write-order 队列。
- 缓冲:读缓冲记录访问痕迹,写缓冲记录 add/update/remove 任务。
- 维护组件:
evictionLock、FrequencySketch、TimerWheel、reference queues、scheduler/pacer。
节点生命周期:
stateDiagram-v2
[*] --> Alive: 加入 data 和策略结构
Alive --> Retired: 从 data 移除但仍等待策略结构清理
Retired --> Dead: 从策略队列/时间轮移除
Alive --> Dead: 维护任务直接完成清理
Dead --> [*]
读流程
getIfPresent 是最典型的读路径。无边界 cache 只查 ConcurrentHashMap 并记录统计;有边界 cache 还要检查引用、过期、访问时间、读缓冲和刷新。
flowchart TD
A["getIfPresent(key)"] --> B["data.get(lookupKey)"]
B -- 未命中 --> C[记录 miss]
C --> D{维护状态需要 drain?}
D -- 是 --> E[scheduleDrainBuffers]
D -- 否 --> F[返回 null]
E --> F
B -- 命中 node --> G[读取 value 和当前时间]
G --> H{value 为 null 或已过期?}
H -- 是 --> I[记录 miss 并调度维护]
I --> F
H -- 否 --> J[更新 accessTime/variableTime]
J --> K[afterRead]
K --> L[记录 hit]
L --> M[写入 readBuffer]
M --> N{需要 drain?}
N -- 是 --> E
N -- 否 --> O{refreshAfterWrite 到期?}
E --> O
O -- 是 --> P[触发异步 refresh]
O -- 否 --> Q[返回当前 value]
P --> R{refresh 已同步完成?}
R -- 是 --> S[返回新 value]
R -- 否 --> Q
读缓冲是有意的近似机制:读路径不直接持有 evictionLock 修改策略队列,而是把访问事件放入 BoundedBuffer。缓冲满或维护状态要求时,再异步合并到策略结构。
写流程
put、compute、merge 这类写操作先通过 ConcurrentHashMap 原子修改主索引和节点内容,再把策略更新包装成 AddTask、UpdateTask 或 RemovalTask 放入写缓冲。
flowchart TD
A[put/compute/merge] --> B[校验 key/value]
B --> C[计算 lookupKey/referenceKey]
C --> D{data 中已有 node?}
D -- 否 --> E[创建 Node 并设置时间/权重]
E --> F[data.putIfAbsent 或 compute 插入]
F --> G[afterWrite AddTask]
D -- 是 --> H[同步 node]
H --> I{旧值失效/过期/被收集?}
I -- 是 --> J[按新建语义写入并记录 eviction cause]
I -- 否 --> K[按更新语义替换 value/weight/time]
J --> L[afterWrite UpdateTask 或 RemovalTask]
K --> L
G --> M[写任务进入 writeBuffer]
L --> M
M --> N{可异步调度?}
N -- 是 --> O[scheduleAfterWrite/scheduleDrainBuffers]
N -- 否 --> P[当前线程获取 evictionLock 执行 maintenance]
写缓冲与读缓冲不同:写任务不能丢。写缓冲 offer 多次失败后,当前写线程会尝试直接执行维护,保证策略结构最终追上主表状态。
维护流程
所有有边界能力的复杂工作都集中在 maintenance 中,并由 evictionLock 串行保护。维护可以由写后调度、读缓冲满、显式 cleanUp()、scheduler/pacer 或写线程兜底触发。
flowchart TD
A[scheduleDrainBuffers 或 cleanUp] --> B[获取 evictionLock]
B --> C[maintenance]
C --> D[drainReadBuffer: 应用访问事件]
D --> E[drainWriteBuffer: 执行 Add/Update/Removal]
E --> F[drainKeyReferences]
F --> G[drainValueReferences]
G --> H[expireEntries]
H --> I[evictEntries]
I --> J[climb: 自适应调整 window 大小]
J --> K{还有未处理任务?}
K -- 是 --> L[状态置 REQUIRED 并重新调度]
K -- 否 --> M[状态回到 IDLE]
drainStatus 是维护调度状态机,用于避免重复提交过多维护任务,同时允许维护过程中有新写入时再跑一轮。
淘汰策略
大小或权重上限由 Window TinyLFU 实现:
- 新条目先进入 Window,Window 使用 LRU,偏向保留最近突发访问。
- 从 Window 溢出的 candidate 尝试进入 Main。
- Main 分成 Probation 和 Protected,整体是 Segmented LRU。
- candidate 与 Main Probation 的 victim 比较历史频率,由
FrequencySketch估算。 - 命中 Probation 的条目会晋升到 Protected;Protected 超额时把最老条目降回 Probation。
- Window 与 Main 的比例会通过 hill climbing 根据命中率采样自适应调整。
flowchart LR
A[新写入] --> B[Window LRU]
B -- Window 超额 --> C[候选 candidate]
D[Main Probation victim] --> E{TinyLFU 频率比较}
C --> E
E -- candidate 更值得保留 --> F[evict victim]
F --> G[candidate 进入 Main Probation]
E -- victim 更值得保留 --> H[evict candidate]
G -- 被访问 --> I[Main Protected]
I -- Protected 超额 --> D
FrequencySketch 是 4-bit Count-Min Sketch,并周期性衰减计数,让历史热点随时间淡出。
过期策略
Caffeine 支持三类过期:
expireAfterAccess:按访问时间过期,依赖 access-order 队列。expireAfterWrite:按写入时间过期,依赖 write-order 队列。expireAfter(Expiry):每个条目可变过期时间,依赖TimerWheel。
固定访问/写入过期可以通过队首判断是否还有更年轻条目;可变过期用层级时间轮把事件分桶,维护时推进时间轮并批量处理到期 bucket。
flowchart TD
A[expireEntries] --> B{expireAfterAccess?}
B -- 是 --> C[扫描 access-order 队首]
B -- 否 --> D{expireAfterWrite?}
C --> D
D -- 是 --> E[扫描 write-order 队首]
D -- 否 --> F{variable expiry?}
E --> F
F -- 是 --> G[TimerWheel.advance]
G --> H[evictEntry cause=EXPIRED]
F -- 否 --> I[完成]
H --> I
过期清理是摊销和尽力的:读写操作、cleanUp() 或 Scheduler 会推动维护;过期条目不会对正常读写可见,但可能暂时计入 estimatedSize()。
刷新与加载
LoadingCache.get(key) 走 cache().computeIfAbsent(key, mappingFunction()),保证同一 key 的加载通过 ConcurrentHashMap 原子计算合并。refreshAfterWrite 不会主动后台刷新,而是在读到旧值且超过刷新间隔时触发异步 reload。
刷新流程的关键点:
- 读请求先返回旧值,刷新在后台执行。
refreshes()记录同一 key 的在途刷新,避免重复 reload。- reload 完成后通过
compute写回;如果期间用户已经修改该 key,则丢弃刷新结果。 - 异步 cache 中,条目值本身是
CompletableFuture<V>;失败、取消或完成为null的 future 会被移除。
sequenceDiagram
participant User
participant Cache
participant Loader
participant Map as data/refreshes
User->>Cache: get(key)
Cache->>Map: 查当前 node/value
alt miss
Cache->>Map: computeIfAbsent
Map->>Loader: load/asyncLoad
Loader-->>Map: value/future
Map-->>Cache: 保存结果
Cache-->>User: 返回 value/future
else hit 且 refresh 到期
Cache->>Map: refreshes.computeIfAbsent
Cache->>Loader: asyncReload(oldValue)
Cache-->>User: 立即返回 oldValue
Loader-->>Cache: newValue
Cache->>Map: compute 写回,带 ABA 检查
else hit
Cache-->>User: 返回当前 value
end
引用强度与回收
默认 key/value 都是强引用。开启 weakKeys、weakValues 或 softValues 后:
NodeFactory会选择不同节点实现。- 主表 key 可能是弱引用对象,lookup 时使用临时 lookup reference。
- value 可能是弱/软引用。
ReferenceQueue在维护阶段被 drain,收集到的条目按RemovalCause.COLLECTED移除。- 弱 key 或弱/软 value 会改用 identity 语义。
并发模型
读写正确性主要由三层机制配合:
ConcurrentHashMap提供主索引并发访问和每 key/bin 的原子计算。synchronized(node)保护节点内部 value、weight、时间戳和生命周期变更。evictionLock串行保护策略结构,例如队列、时间轮、频率 sketch、policy weight 和全局权重计数。
高吞吐设计的关键是:用户读写优先更新主表,策略数据结构通过缓冲异步追赶,因此淘汰、过期和统计在可接受范围内是最终一致的。
统计与通知
StatsCounter 记录 hit、miss、load success/failure、load time、eviction count/weight。默认关闭,recordStats() 后使用 ConcurrentStatsCounter。
通知分两类:
removalListener:所有移除原因都会通知,通常通过 executor 异步执行。evictionListener:只关心自动淘汰/过期/收集等 eviction,调用位置更靠近内部状态变更,使用时要避免重入 cache 写操作。
移除原因由 RemovalCause 表示,包括 EXPLICIT、REPLACED、COLLECTED、EXPIRED、SIZE。
一次完整的 bounded cache 生命周期
flowchart TD
A[创建 builder] --> B[build 生成具体 cache]
B --> C[读写请求进入 Local*Cache 骨架]
C --> D[LocalCache 操作 data 主表]
D --> E[Node 保存值和元数据]
E --> F[读事件进入 readBuffer]
E --> G[写事件进入 writeBuffer]
F --> H[maintenance 合并事件]
G --> H
H --> I[更新 Window/Main 队列]
H --> J[推进过期队列/TimerWheel]
H --> K[处理引用队列]
I --> L{容量/权重超限?}
J --> M{条目过期?}
K --> N{key/value 被 GC?}
L -- 是 --> O[evictEntry SIZE]
M -- 是 --> P[evictEntry EXPIRED]
N -- 是 --> Q[evictEntry COLLECTED]
O --> R[更新统计并发送通知]
P --> R
Q --> R
阅读建议
- 先读
Caffeine.java的build*方法,理解实例创建分支。 - 再读
LocalManualCache、LocalLoadingCache、LocalAsyncCache,理解公开 API 如何落到底层。 - 对照
UnboundedLocalCache看最小实现,它展示了没有策略时的行为。 - 重点读
BoundedLocalCache顶部大注释、getIfPresent、put、remap、afterRead、afterWrite、maintenance、evictEntries、expireEntries。 - 最后读
NodeFactory、LocalCacheFactory和src/javaPoet,理解为什么很多具体类不在src/main里。
