当前位置: 首页 > news >正文

【缓存】本地缓存Caffeine AsyncCache实现原理与最佳实践

AsyncCache:JVM 级 SingleFlight 实现原理与最佳实践

本文基于CaffeineAsyncCache+Java 8+,适用于需要高并发下防止缓存击穿、避免重复加载的场景。


一、核心目标:同一 Key 只加载一次

在高并发场景下,缓存失效瞬间可能出现缓存击穿(Cache Breakdown)

大量线程同时发现缓存缺失,同时去 DB / RPC 加载同一份数据。

AsyncCache的目标就是:无论多少线程并发访问,同一个 Key 只触发一次加载逻辑


二、核心实现原理

1. 原子性保障:ConcurrentHashMap.computeIfAbsent

AsyncCache底层依赖ConcurrentHashMap,核心逻辑等价于:

CompletableFuture<V>future=map.computeIfAbsent(key,k->{// ✅ 同一时刻,只有一个线程能进入此处CompletableFuture<V>f=newCompletableFuture<>();executor.execute(()->{try{Vvalue=loadFromDb(k);f.complete(value);}catch(Throwablet){f.completeExceptionally(t);map.remove(k,f);// 加载失败,允许重试}});returnf;});
关键保证
特性说明
原子性computeIfAbsent对同一 key 所在桶加锁,保证创建 Future 是原子操作
可见性Node.valuenextvolatile修饰,Happens-Before 规则保证对其他线程立即可见
唯一性同一 key 永远只创建一个CompletableFuture
等待机制未抢到锁的线程直接拿到已有 Future,自然等待结果

这就是 JVM 级的 SingleFlight 实现


2. Java 8 的并发控制:CAS +synchronized桶级锁

⚠️重要更正:Java 8 中ConcurrentHashMap已废弃 JDK 7 的 Segment 分段锁(Striped Locking),改用CAS 无锁 +synchronized桶级锁的混合策略。

JDK 7 vs JDK 8 对比
维度JDK 7(已淘汰)JDK 8+(当前主流)
数据结构Segment[]+HashEntry[]+ 链表Node[]+ 链表 / 红黑树
锁机制ReentrantLock分段锁(Striped Locking)CAS +synchronized桶级锁
锁粒度Segment 级别(默认 16 段)单个桶的头节点(并发度 = 数组长度)
读操作volatile保证可见性volatile保证可见性(完全无锁)
写操作先获取 Segment 锁先 CAS 尝试,失败再synchronized锁桶
扩容单个 Segment 独立扩容多线程协同扩容(ForwardingNode标记)
哈希冲突退化纯链表 O(n)链表 ≥ 8 且容量 ≥ 64 时转红黑树 O(log n)
computeIfAbsent的执行流程(Java 8)
线程 T1、T2、T3 同时调用 computeIfAbsent("sameKey", ...) │ ▼ ① 计算 hash,定位到桶下标 i │ ▼ ② 桶为空?── 是 ──→ CAS 直接插入(无锁快路径) │ 否 ▼ ③ synchronized 锁住桶的头节点 │ ▼ ④ 再次检查 key 是否仍不存在(double check) │ ▼ ⑤ 只有一个线程执行 mappingFunction │ ▼ ⑥ 释放锁,其他线程拿到同一个 Future

核心要点

  1. CAS 优先:无竞争时完全无锁,性能极高
  2. synchronized锁桶头节点:只有发生哈希冲突时才加锁,且锁粒度极小
  3. 锁升级机制:JVM 会自动将synchronized从偏向锁 → 轻量级锁 → 重量级锁逐步升级,绝大多数场景停留在轻量级锁阶段
  4. 不同桶之间完全无竞争:并发度约等于桶数组长度(默认 16,可随扩容增长)
为什么 Java 8 选择synchronized而非ReentrantLock
对比项ReentrantLock(JDK 7)synchronized(JDK 8)
锁粒度Segment 级(较粗)桶头节点级(更细)
JVM 优化无特殊优化偏向锁、轻量级锁、自旋、锁消除、锁粗化
内存开销每个 Segment 一个锁对象锁信息内嵌在对象头中,零额外对象
可中断支持lockInterruptibly()不支持(但缓存场景不需要)
公平性可配置不可配置(但 FIFO 等待已足够)
实际性能较好更优(尤其高并发 + 短临界区)

结论:在ConcurrentHashMap这种锁持有时间极短、不需要条件变量和中断特性的场景下,synchronized经过 JVM 优化后性能全面优于ReentrantLock,且零内存开销。


3. 自动清理与内存安全

  • Future 完成后,引用由 GC 自动回收
  • 加载失败时主动remove(key),避免:
    • 空值缓存
    • 永久阻塞
    • 内存泄漏

三、推荐写法

✅ 基础用法(最推荐)

publicStringgetData(Stringkey){returnasyncCache.get(key,(k,exec)->CompletableFuture.supplyAsync(()->loadDataFromDb(k),exec)).join();}
  • exec:Caffeine 内置ForkJoinPool生产环境建议自定义线程池
  • join():阻塞等待结果,适合非响应式服务

✅ 带超时保护(防止线程堆积)

publicStringgetDataWithTimeout(Stringkey){returnasyncCache.get(key,(k,exec)->CompletableFuture.supplyAsync(()->loadDataFromDb(k),exec).completeOnTimeout("fallback",2,TimeUnit.SECONDS)).join();}

✅ 防止:

  • DB 慢查询
  • RPC 无限阻塞
  • 线程池被打爆

✅ 防止缓存污染(加载失败不缓存)

publicStringgetDataSafe(Stringkey){returnasyncCache.get(key,(k,exec)->CompletableFuture.supplyAsync(()->loadDataFromDb(k),exec).exceptionally(ex->{asyncCache.synchronous().invalidate(k);thrownewRuntimeException(ex);})).join();}

📌非常重要:否则失败结果会被缓存,导致后续请求全部失败。


五、原子更新 Value

如果你是想做CAS 风格更新

cache.asMap().compute(key,(k,oldFuture)->{if(oldFuture==null){returnCompletableFuture.completedFuture("init");}returnoldFuture.thenApply(v->v+"_updated");});

⚠️ 注意:

  • AsyncCache中 Value 是CompletableFuture
  • 更新成本较高,通常不建议频繁使用

六、并发测试验证

publicstaticvoidmain(String[]args){AsyncCache<String,String>cache=Caffeine.newBuilder().buildAsync();AtomicIntegerloadCount=newAtomicInteger(0);List<CompletableFuture<String>>futures=IntStream.range(0,10).parallel().mapToObj(i->cache.get("sameKey",(key,exec)->{System.out.println(Thread.currentThread().getName()+" loading...");loadCount.incrementAndGet();returnCompletableFuture.supplyAsync(()->"Data-"+key);})).toList();futures.get(0).whenComplete((v,ex)->{System.out.println("Result: "+v);});System.out.println("Load count = "+loadCount.get());// ✅ 永远是 1}

典型输出

ForkJoinPool.commonPool-worker-1 loading... Result: Data-sameKey Load count = 1

完美证明:仅一个线程执行加载逻辑


七、与 Redis 分布式锁对比

维度AsyncCache(JVM 级)Redis Lock(分布式)
作用范围单 JVM 内跨 JVM / 跨机器
锁机制CAS +synchronized桶级锁SETNX / Redlock
性能⭐⭐⭐⭐⭐(纳秒~微秒级)⭐⭐(毫秒级 + 网络 IO)
复杂度低(开箱即用)高(需处理超时、死锁、脑裂)
网络 IO有(每次加锁至少 1 次 RTT)
适用场景单机本地缓存防击穿分布式协调、跨服务互斥

结论:能使用AsyncCache解决的场景,不要用 Redis 锁。二者不是替代关系,而是互补——分布式场景仍需 Redis,单机高并发场景AsyncCache是更优解。


八、避坑指南

❌ 坑 1:在computeIfAbsent的 Lambda 中再次操作同一个 Map

// 错误示范:可能导致死锁map.computeIfAbsent("key",k->{map.put("otherKey",someValue);// ⚠️ Lambda 内可能持有桶锁,再次操作可能死锁returncomputeValue();});

正确做法:Lambda 内只做纯计算,不涉及任何 Map 写操作。

❌ 坑 2:mappingFunction返回null

computeIfAbsentmappingFunction不允许返回 null,否则抛出NullPointerException

// 错误cache.get(key,k->null);// NPE!// 正确:返回包装类型或 Optionalcache.get(key,k->CompletableFuture.completedFuture(null));// OK

[citation:6]

❌ 坑 3:mappingFunction执行时间过长

computeIfAbsent在执行 Lambda 时持有桶锁(虽然时间极短),如果 Lambda 内做耗时操作,会阻塞同一桶的其他操作。

正确做法:Lambda 内只创建CompletableFuture实际计算交给异步线程

// ✅ 推荐:Lambda 立即返回 Future,计算异步执行cache.get(key,(k,exec)->{returnCompletableFuture.supplyAsync(()->loadDataFromDb(k),exec);});

九、总结

CaffeineAsyncCache通过ConcurrentHashMap.computeIfAbsent实现了 JVM 级的 SingleFlight。在 Java 8 中,底层依赖 CAS 无锁 +synchronized桶级锁的混合策略,以极低的锁开销保证同一 Key 只加载一次。

核心要点速查

要点一句话说明
原子性来源ConcurrentHashMap.computeIfAbsent(Java 8:CAS +synchronized桶级锁)
锁粒度单个桶的头节点,不同桶之间零竞争
无锁路径桶为空时 CAS 直接插入,完全无锁
等待机制所有线程共享同一个CompletableFuture
失败处理主动remove,允许重试
超时保护completeOnTimeout防止永久阻塞
分布式场景仍需 Redis / DB 层协调
最大陷阱Lambda 内不要操作同一个 Map,不要返回 null

十、参考与延伸阅读

  • JDK 源码ConcurrentHashMap.computeIfAbsent()(JDK 8u60+)
  • Caffeine 官方文档:https://github.com/ben-manes/caffeine
  • Java 8synchronized锁升级机制:偏向锁 → 轻量级锁 → 重量级锁
  • CompletableFuture超时 API:Java 9+completeOnTimeout/ Java 8 GuavaFutures.withTimeout

http://www.jsqmd.com/news/1328361/

相关文章:

  • 二叉树右视图:BFS与DFS算法解析与应用
  • 线程池配置实战:从原理到高并发避坑指南
  • 跨省转诊医疗护送,持大型活动救护保障资质更放心 - AZJ888
  • C语言数据类型详解:从基础到应用实践
  • Logisim-evolution时序仿真终极指南:掌握数字电路时钟域设计实战技巧
  • 2026年家装门窗市场观察:系统窗普及加速,政泰门窗以制造能力承接存量改造需求 - 优企名品
  • Obsidian个性化首页终极指南:5分钟打造你的专属知识工作台
  • Python+Vue3构建大学生就业求职系统实战
  • 2026年8月|封边机工厂**推荐指南 - 资讯在线
  • 郑州空气源热泵服务商推荐:【芬尼】一站服务 - 17328623207
  • IT9205分布式矩阵延长芯片方案
  • 朱雀 AIGC 检测原理是什么?能准确识别文章 AI 率和去 AI 痕迹效果吗?
  • 独家策略:深圳慧新提出的“GEO+SEO”双引擎驱动模型
  • 如何快速解决Windows热键冲突:终极检测工具使用指南
  • 小微企业适合部署语音机器人吗?来电语音客服机器人轻量化方案
  • 武汉企业主与高净值人群离婚律师**:股权分割、隐匿财产追踪与敏感身份体面离婚实战指南 - 商讯
  • 2026 东莞翡翠回收品相评估体系落地:种质分级细化,专业机构定价公允可查 - 二奢分享官
  • 2026年云南留学国家选择规划哪家靠谱:五家优选深度解析 - 科技焦点
  • 限时解锁|全国TOP5教培机构正在用的AI错题分级算法(含真实学情衰减曲线建模)
  • 2026年8月|封边机品牌**** - 资讯在线
  • 郑州空气能暖气品牌推荐:【芬尼】暖意绵长 - 17728098551
  • 2026 年新消息:长沙靠谱的锁脚锚杆注浆加固加工厂选哪家,给隧道施工加道安全闸?你不知道的这招细节竟能避免后期大隐患。 - 品质体验官
  • Hortic Res项目文章|GWAS挖出猕猴桃高维C基因:转录因子AebHLH89激活AeGMP1促进L-AsA积累
  • 终极Windows和Office免费激活指南:5分钟实现永久激活
  • AI如何自动识别废标后还能参与重新招标废标风险?智能评审项目实践
  • 2026年成都全屋定制品牌**单:深度拆解本地工厂与全国连锁的性价比真相 - 官方资讯
  • 2026信息管理专业毕业设计选题指南与技术方案
  • STM32 DMA存储器到存储器传输实战:从原理到代码配置详解
  • 知网CAJ文件怎么转PDF?整理几种实测可用的转换方法 - 软件小管家
  • 配电网最优潮流计算:二阶锥松弛技术与工程实践