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() 返回 Cache
  • build(CacheLoader) 返回 LoadingCache
  • buildAsync() 返回 AsyncCache
  • buildAsync(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 任务 generateLocalCachesgenerateNodes 将代码生成到 build/generated/sources/local-cachebuild/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 任务。
  • 维护组件:evictionLockFrequencySketchTimerWheel、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。缓冲满或维护状态要求时,再异步合并到策略结构。

写流程

putcomputemerge 这类写操作先通过 ConcurrentHashMap 原子修改主索引和节点内容,再把策略更新包装成 AddTaskUpdateTaskRemovalTask 放入写缓冲。

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 都是强引用。开启 weakKeysweakValuessoftValues 后:

  • 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 表示,包括 EXPLICITREPLACEDCOLLECTEDEXPIREDSIZE

一次完整的 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

阅读建议

  1. 先读 Caffeine.javabuild* 方法,理解实例创建分支。
  2. 再读 LocalManualCacheLocalLoadingCacheLocalAsyncCache,理解公开 API 如何落到底层。
  3. 对照 UnboundedLocalCache 看最小实现,它展示了没有策略时的行为。
  4. 重点读 BoundedLocalCache 顶部大注释、getIfPresentputremapafterReadafterWritemaintenanceevictEntriesexpireEntries
  5. 最后读 NodeFactoryLocalCacheFactorysrc/javaPoet,理解为什么很多具体类不在 src/main 里。