知识卡片

Caffeine用环形缓冲把读竞争转移到异步日志提交

结构图卡

内容

进程内缓存的吞吐量瓶颈往往不在读写数据本身(HashMap本就是O(1)),而在 “读取的同时还要维护统计状态”——每次访问都要更新最近访问时间、计数器 这类元信息,用来支撑LRU/LFU淘汰策略。Guava Cache的思路是同步处理:访问 数据时顺带在锁保护下更新这些状态,靠分段加锁减少竞争。Caffeine的思路 不同:借鉴数据库把”数据修改”看作”日志提交”的设计,读取时数据本身仍从 ConcurrentHashMap直接返回,但状态变更信息不去抢Map上的锁,而是写入一个 环形缓冲区(Ring Buffer)——为进一步减少竞争,甚至给每个线程哈希值分配 专属缓冲区,由后台线程异步批量处理这些状态变更。环形缓冲的巧妙之处在于 读写指针在一段连续内存里循环推进,只要读指针不被写指针追上就能无限复用 空间。代价是这条路径”有损”:如果异步处理跟不上变更速度、缓冲区满了, 新的状态变更信息会被直接丢弃,直到缓冲区腾出空间为止——Caffeine用容忍 少量统计精度损失,换来了几乎与ConcurrentHashMap持平的读取性能。写入 路径则相反:用无损的有界队列存放变更信息,因为许多状态的默认值必须通过 写入操作完成初始化,不能丢,因此写入会比ConcurrentHashMap慢约10%。

结构图

flowchart LR
    R[读取数据] --> M[直接从ConcurrentHashMap返回, 与读ConcurrentHashMap同速]
    R -.伴随的状态变更.-> RB[环形缓冲区 Ring Buffer<br/>按线程哈希分区]
    RB -->|后台线程异步批处理| S[更新访问时间/计数器状态]
    RB -.缓冲区满时.-> D[直接丢弃, 容忍精度损失]
    W[写入数据] --> Q[有界队列 ArrayQueue]
    Q -->|无损, 必须处理| S

参考来源

- 位置:《凤凰架构:构建可靠的大型分布式系统》第4章"透明多级分流系统" 4.6.1节"缓存属性"(源文件:_epub-src对应OEBPS/Text/chapter54.xhtml) - 结论依据:原文说明Caffeine用环形缓冲记录读取产生的状态变动日志、由 后台线程异步处理,缓冲区满时直接丢弃变更信息,读取性能因此接近 ConcurrentHashMap,而写入路径用无损的有界队列导致约10%的性能损失, 直接支撑本卡片结构梳理。 - 原始内容:通过环形缓冲和容忍有损失的状态变更,Caffeine大幅降低了由于 数据读取而导致的垃圾收集和锁竞争,因此Caffeine的读取性能几乎能与 ConcurrentHashMap的读取性能相同……相比ConcurrentHashMap,Caffeine在 写入时大约会慢10%左右。