摘要:相信大家对Caffeine都不陌生,Caffeine 作为高性能的 java 缓存库,被广泛应用于各种 Java 框架、中间件、数据库访问层等场景,大部分高并发的项目都用到Caffeine本地缓存,以提升响应速度、降低数据库压力、优化高并发性能。下面我们深入了
相信大家对Caffeine都不陌生,Caffeine 作为高性能的 java 缓存库,被广泛应用于各种 Java 框架、中间件、数据库访问层等场景,大部分高并发的项目都用到Caffeine本地缓存,以提升响应速度、降低数据库压力、优化高并发性能。下面我们深入了解下Caffeine......
在高并发系统中,缓存是提升性能的核心组件之一。Caffeine 作为一款高性能 Java 缓存库,凭借其 Window-TinyLFU 淘汰算法 和 卓越的并发性能,成为许多系统的首选。本文将深入解析 Caffeine 的设计原理、核心架构、关键 API,并通过一个高并发场景的实战案例,展示其应用方法。
淘汰策略:Window-TinyLFU ,Caffeine 采用 Window-TinyLFU 算法,结合了 LRU(最近最少使用)和 LFU(最不经常使用)的优点 #技术分享: 滑动窗口分区: 缓存分为一个 主区域(保护高频数据)和一个 窗口区域(接纳新数据),避免突发流量污染缓存。 频率素描(Count-Min Sketch): 以极低的内存开销统计数据访问频率,替代传统 LFU 的哈希计数。高性能并发设计分段锁机制: 写操作分段加锁,减少线程竞争。 无锁读优化: 通过 AtomicReference 实现并发读的高性能。关键数据结构设计
class CountMinSketch { long matrix = new long[4]; void increment(key) { for (int i = 0; i { K key; V value; volatile long accessTime; int frequency; Node prev, next; }下面我给出读请求和写请求两种模块内部工作交互
用户调用 Cache.get("key")查 ConcurrentHashMap → 命中?返回数据&频率+1 : 继续未命中 → 调用 CacheLoader.load("A") 加载数据数据加载后:写入 ConcurrentHashMap更新 Count-Min Sketch(频率+1)返回数据调用 cache.put(key, value)锁获取: 根据 key.hashCode 计算对应的 Segment 锁(默认16个分段)锁内操作:步骤1: 检查 Key 是否已存在:存在 → 覆盖旧值;不存在 → 进入 Window-TinyLFU 淘汰逻辑
步骤2:
对比新数据与 Window 中最旧数据的频率素描计数。新数据频率更高 → 淘汰旧数据,新数据进入 Main 区。旧数据频率更高 → 丢弃新数据。
4.
记录访问时间戳(LRU 逻辑)
释放锁: 完成写入下面给出伪代码
if (window.isFull) { Candidate newEntry = new Candidate(key, value); Candidate victim = window.getOldestEntry; if (frequencySketch.compare(newEntry, victim) > 0) { mainRegion.admit(newEntry); window.evict(victim); }}| 模块 | 功能描述 | | ---
| Cache Interface | 定义缓存的核心 API(get、put、invalidate 等)。| | Eviction Policy | 实现 Window-TinyLFU 算法,管理缓存淘汰逻辑。| | Concurrency Control | 通过分段锁和 CAS 操作保证线程安全。| | Data Storage | 基于 ConcurrentHashMap 存储缓存条目,支持高效查找。|
Cache cache = Caffeine.newBuilder .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) .expireAfterAccess(5, TimeUnit.MINUTES) .refreshAfterWrite(1, TimeUnit.MINUTES) .weakKeys .weakValues .recordStats .removalListener((key, value, cause) ->System.out.println("移除原因: " + cause)) .build;关键参数:
maximumSize:触发淘汰的条目数(非字节大小,需结合 weigher 使用)。-
Write:严格一致性,适合配置类数据。Access:更高命中率,适合热点数据。
refreshAfterWrite:异步刷新(旧值仍可用),避免缓存雪崩。cache.put("key", new Object);底层逻辑:
计算 key.hashCode 确定所属 Segment(默认16个分段)。获取分段锁(ReentrantLock),保证线程安全。执行写入:若 Key 存在,直接覆盖。若 Key 不存在,进入 Window-TinyLFU 淘汰流程:if (window.isFull) { if (frequencySketch.compare(newData, oldestData) > 0) { mainRegion.admit(newData); }}更新 Count-Min Sketch 频率统计 (无锁CAS操作) 。释放 分段锁 。3. 数据读取:get(K key, Function loader)Object value = cache.get("key", k -> loadFromDB(k));执行流程:
无锁读:尝试从 ConcurrentHashMap 获取值(get 为原子操作)。2.
同步调用 loader.apply(key) 加载数据(同一 Key 并发时仅加载一次)并写入缓存并更新频率素描。
缓存命中:更新 LRU 访问时间戳,返回缓存值。高并发优化: 使用 AsyncLoadingCache 避免阻塞:
AsyncLoadingCache asyncCache = Caffeine.newBuilder .buildAsync(key -> loadFromDB(key));CompletableFuture future = asyncCache.get("key");cache.invalidate("key");cache.invalidateAll;自动淘汰触发条件:
基于大小(maximumSize):触发 Window-TinyLFU 淘汰赛。基于时间(expireAfter*):后台线程定期清理(ForkJoinPool)。基于引用(weakKeys/weakValues):依赖 GC 回收。.removalListener((key, value, cause) -> { if(cause != RemovalCause.EXPLICIT && cause != RemovalCause.REPLACED && cause != RemovalCause.SIZE) listener.notifyExpired(busId, (String)key);})recordStats启用缓存统计功能,记录以下核心指标:
CacheStats {
hitCount; // 缓存命中次数
missCount; // 缓存未命中次数
loadSuccessCount; // 成功加载新值的次数
loadFailureCount; // 加载失败次数
totalLoadTime; // 总加载耗时(纳秒)
evictionCount; // 因容量或过期导致的淘汰总数
evictionWeight; // 淘汰条目的总权重(仅在使用weigher时有效)
}
CacheStats stats = cache.stats;System.out.printf("命中率: %.1f%%, 加载次数: %d, 淘汰数: %d", stats.hitRate * 100, stats.loadCount, stats.evictionCount);hitRate:缓存命中率(0~1)。loadSuccessCount:成功加载次数。totalLoadTime:总加载耗时(纳秒)。cache.refresh("key");与 expire 的区别:expire:立即失效,请求需等待新值加载。refresh:后台加载,旧值可继续服务(更平滑)。
源码性能优化技巧频率素描(Count-Min Sketch): 使用 4 个 long 矩阵统计频率,误差率 分段锁优化: 默认 16 个 Segment,并发写性能接近 O(1)。无锁读路径: 所有读操作完全无锁(volatile 变量 + AtomicReference)。
场景: 每秒万级请求的读请求(商品查询,架构查询),要求 99.9% 的缓存命中率。防止缓存击穿、雪崩,支持平滑过期刷新。
import com.github.benmanes.caffeine.cache.*;import java.util.concurrent.*;import java.util.concurrent.atomic.AtomicBoolean;public class ProductCache { private static final LoadingCache cache = Caffeine.newBuilder .maximumSize(10000) .expireAfterWrite(30, TimeUnit.MINUTES) .refreshAfterWrite(5, TimeUnit.MINUTES) .executor(Executors.newFixedThreadPool(4)) .recordStats .build(new ProductLoader);private static final ConcurrentMap loadingLocks = new ConcurrentHashMap;private static class ProductLoader implements CacheLoader { @Override public Product load(Long productId) throws Exception { return fetchFromDB(productId); }@Override public Product reload(Long productId, Product oldValue) throws Exception { return fetchFromDB(productId); }private Product fetchFromDB(Long productId) { AtomicBoolean lock = loadingLocks.computeIfAbsent(productId, k -> new AtomicBoolean(false)); while (!lock.compareAndSet(false, true)) { Thread.yield; }try { Product cached = cache.getIfPresent(productId); if (cached != null) return cached;System.out.println("Loading from DB: " + productId); Thread.sleep(100); return new Product(productId, "Product-" + productId, ThreadLocalRandom.current.nextInt(100));} catch (Exception e) { throw new RuntimeException("DB error", e); } finally { lock.set(false); loadingLocks.remove(productId); } } }public static class Product { private final Long id; private final String name; private final int stock;public Product(Long id, String name, int stock) { this.id = id; this.name = name; this.stock = stock; } }public static Product getProduct(Long productId) { return cache.get(productId); }public static void main(String args) throws InterruptedException { ExecutorService pool = Executors.newFixedThreadPool(20); for (int i = 0; i { Product p = getProduct(productId); System.out.println("Get: " + p.name); }); } pool.shutdown; pool.awaitTermination(1, TimeUnit.SECONDS);System.out.println("缓存命中率: " + cache.stats.hitRate); } }设计思路防击穿: 使用 Caffeine 的 CacheLoader + 同步锁(loadingLocks 的 AtomicBoolean CAS 操作),保证单个 Key 只加载一次 防雪崩: refreshAfterWrite 异步刷新 + 随机过期时间 热点保护: 基于 maximumSize 和 Window-TinyLFU 自动管理热点数据
我们在使用 Caffine 时,根据 内存压力 调整 maximumSize,避免频繁淘汰。Caffeine 通过创新的 Window-TinyLFU 算法 和 精细化并发控制 ,在缓存命中率和吞吐量之间取得平衡。本文的实战案例展示了如何通过异步加载、自动刷新等机制,构建稳定高效的缓存层。
适用场景: 读多写少、高并发、低延迟需求的业务(如电商、社交平台)。
来源:墨码行者