《异步加载也翻车?Caffeine缓存把ForkJoinPool搞崩溃的奇幻漂流》
各位缓存探险家们,今天我们要聊一个比刺激的故事——caffine异步加载高枕无忧吗?Too young too simple!当Caffeine遇上ForkJoinPool,就像把大象塞进冰箱,结局往往出人意料...
案发现场:异步加载的"甜蜜陷阱"
先看这位"受害者"的代码:
AsyncLoadingCache<String, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000)
..refreshAfterWrite(1, TimeUnit.MINUTES) // 写入1分钟后触发异步刷新
.buildAsync(key -> {
// 模拟耗时IO操作
try { Thread.sleep(100); } // 实际是redis 查询等IO操作
catch (InterruptedException e) { /* 忽略 */ }
return db.getProduct(key);
}); // 默认使用ForkJoinPool.commonPool()
// 业务代码
public CompletableFuture<Product> getProduct(String id) {
return cache.get(id);
}
看起来不存在同步阻塞的问题,但是...当并发量上来时:线上大量如下报错
java.util.concurrent.RejectedExecutionException:
Thread limit exceeded replacing blocked worker
上述报错解释:
- 工作线程全部被阻塞
- 补偿机制尝试创建新线程但达到上限
- 系统彻底"死锁"——没有线程能执行任务了
原因分析: 根本原因是caffine没有配置线程池,使用了默认线程池(ForkJoinPool),导致无法创建线程,死锁
原理深潜:ForkJoinPool的"七宗罪"
1. 公共池的"共享经济"模式
默认的ForkJoinPool.commonPool()是个共享资源:
- 被整个JVM中所有并行流、CompletableFuture等共享
- 默认线程数 = CPU核心数 - 1(比如8核机器只有7个线程)
这就像用一家奶茶店的外卖小哥服务全城所有奶茶店,高峰期不爆单才怪!
2. 工作窃取算法的"死锁"陷阱
ForkJoinPool使用工作窃取(work-stealing)算法:
- 每个线程有自己的工作队列
- 空闲线程可以"偷"其他线程队列中的任务
但当所有线程都在等待缓存加载完成时:
// 伪代码展示问题
mainThread: 提交任务A到ForkJoinPool → 等待A完成
ForkJoinWorkerThread-1: 执行A → 需要加载key1 → 提交子任务B
ForkJoinWorkerThread-1: 等待B完成 → 尝试窃取其他任务...
// 但所有线程都在等待,形成死锁!
3. 线程饥饿的"饥饿游戏"
当缓存加载任务比可用线程多时:
- 新任务无法分配到线程
- 已分配线程被阻塞在IO上
- 系统吞吐量断崖式下跌
犯罪证据:线程转储分析
查看线程dump会发现大量类似堆栈:
"ForkJoinPool.commonPool-worker-1" #32 daemon prio=5 os_prio=0 tid=0x00007f8a3c0c5000 nid=0x5a1e waiting on condition [0x00007f8a243f6000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000006c0f0b2c8> (a java.util.concurrent.CompletableFuture)
at java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3316)
at java.util.concurrent.CompletableFuture.waitingGet(CompletableFuture.java:1729)
at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
at com.github.benmanes.caffeine.cache.LocalCache.lambda$newAsyncLoadingCache$1(LocalCache.java:143)
解决方案:从"共享经济"到"专车服务"
方案1:自定义专用线程池
// 创建专用线程池
ExecutorService loaderExecutor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2, // 通常2N~4N
new ThreadFactoryBuilder()
.setNameFormat("cache-loader-%d")
.setDaemon(true)
.build());
AsyncLoadingCache<String, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.executor(loaderExecutor) // 关键配置!
.buildAsync(key -> db.getProduct(key));
优点:
- 隔离缓存加载和业务逻辑
- 避免与系统其他组件争抢线程资源
- 可监控线程池状态
监控小技巧:
// 监控线程池状态
ThreadPoolExecutor executor = (ThreadPoolExecutor)loaderExecutor;
logger.info("CacheLoader pool status: {}/{} active/total",
executor.getActiveCount(),
executor.getMaximumPoolSize());
方案2:弹性线程池配置
ExecutorService loaderExecutor = new ThreadPoolExecutor(
10, // 核心线程数
100, // 最大线程数
60, TimeUnit.SECONDS, // 空闲超时
new LinkedBlockingQueue<>(1000), // 缓冲队列
new ThreadFactoryBuilder()
.setNameFormat("cache-loader-%d")
.build(),
new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
关键参数解析:
- 队列容量:建议设置合理上限避免OOM
- 拒绝策略:推荐选择以下之一:
-
- CallerRunsPolicy:让调用线程直接执行(简单但可能影响调用方)
- 自定义策略:如返回兜底值或降级结果
方案3:响应式编程改造
AsyncLoadingCache<String, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.executor(task -> Schedulers.boundedElastic().schedule(task)) // Reactor线程池
.buildAsync(key ->
Mono.fromCallable(() -> db.getProduct(key))
.subscribeOn(Schedulers.parallel())
.toFuture());
适用场景:
- 已使用Reactor/WebFlux的项目
- 需要更精细的线程控制时
深度优化:缓存加载的"降级艺术"
1. 批量加载优化
// 批量加载接口
List<Product> batchGetProducts(List<String> ids);
// 批量加载装饰器
AsyncLoadingCache<String, Product> cache = Caffeine.newBuilder()
.executor(loaderExecutor)
.buildAsync(new AsyncCacheLoader<String, Product>() {
@Override
public CompletableFuture<Product> asyncLoad(String key, Executor executor) {
return CompletableFuture.supplyAsync(() -> db.getProduct(key), executor);
}
@Override
public CompletableFuture<Map<String, Product>> asyncLoadAll(
Iterable<? extends String> keys, Executor executor) {
List<String> idList = StreamSupport.stream(keys.spliterator(), false)
.collect(Collectors.toList());
return CompletableFuture.supplyAsync(
() -> db.batchGetProducts(idList).stream()
.collect(Collectors.toMap(Product::getId, p -> p)),
executor);
}
});
2. 超时与熔断
// 使用Resilience4j添加熔断
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("cacheLoader");
AsyncLoadingCache<String, Product> cache = Caffeine.newBuilder()
.executor(loaderExecutor)
.buildAsync(key -> CircuitBreaker.decorateSupplier(
circuitBreaker,
() -> db.getProduct(key)).get());
3. 监控与告警
// Micrometer监控
registry.gauge("cache.loader.queue.size",
loaderExecutor,
exec -> ((ThreadPoolExecutor)exec).getQueue().size());
registry.gauge("cache.loader.active.threads",
loaderExecutor,
exec -> ((ThreadPoolExecutor)exec).getActiveCount());
知识点扩展:线程池设计的"黄金分割"
- IO密集型 vs CPU密集型:
IO密集型(如缓存加载):线程数 ≈ N*(1 + WT/ST)
WT: 等待时间
ST: 服务时间
N: CPU核心数
CPU密集型:线程数 ≈ N+1
- 队列选择策略:
SynchronousQueue:直接传递,适合快速任务
LinkedBlockingQueue:无界队列,小心OOM
ArrayBlockingQueue:有界队列,需要合理设置大小
- 拒绝策略对比:
策略行为适用场景
AbortPolicy直接抛出异常需要快速失败的场景
CallerRunsPolicy调用者线程执行不想丢弃任务时
DiscardPolicy静默丢弃可容忍丢失的场景
DiscardOldestPolicy丢弃队列最老任务时效性重要的场景
终极配置建议
// 推荐生产环境配置
ExecutorService cacheLoaderExecutor = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(), // 核心线程数
Runtime.getRuntime().availableProcessors() * 4, // 最大线程数
60, TimeUnit.SECONDS, // 空闲超时
new LinkedBlockingQueue<>(1000), // 有界队列
new ThreadFactoryBuilder()
.setNameFormat("cache-loader-%d")
.setUncaughtExceptionHandler((t, e) ->
logger.error("Thread {} failed", t.getName(), e))
.build(),
(r, executor) -> {
logger.warn("Cache loader queue full, rejecting task");
throw new RejectedExecutionException("Cache loader queue overflow");
});
AsyncLoadingCache<String, Product> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.refreshAfterWrite(15, TimeUnit.MINUTES)
.executor(cacheLoaderExecutor)
.recordStats() // 开启统计
.buildAsync(key -> db.getProduct(key));
总结:异步缓存加载的生存法则
- 隔离原则:缓存加载要用专用线程池
- 容量规划:根据业务特点设置合理线程数
- 防御编程:队列、熔断、降级一个不能少
- 监控告警:线程池状态要实时可见
记住:异步不是银弹,用错地方照样让你"异步"转"同步","并发"变"并罚"!你的缓存加载策略,是时候升级到2.0版本了。
本文事故现场还原度99%,剩下1%留给读者自行想象。如果你的系统还没崩,可能只是时候未到...(手动狗头)