湖州市网站建设_网站建设公司_会员系统_seo优化
2026/1/16 19:06:26 网站建设 项目流程

TL;DR

  • 场景:在高并发 Java 服务中使用 Guava Cache 做本地缓存,同时需要控制刷新时延与内存占用。
  • 结论:合理设置 concurrencyLevel + refreshAfterWrite,并理解 LoadingCache 的单 key 加锁语义,可在保证线程安全的前提下拿到接近 ConcurrentHashMap 的并发性能。
  • 产出:给出并发参数选型思路、refresh 阻塞行为理解框架,以及对比自定义 LinkedHashMap LRU 的实现边界,方便在项目中直接落地配置。

Java-187 Guava Cache 并发参数与 refreshAfterWrite 实战:LoadingCache 动态加载与自定义 LRU 全解析

版本矩阵

组件 / 能力版本 / 范围已验证说明
JDK 81.8.x典型存量项目环境,Guava Cache 并发与 refreshAfterWrite 行为一致
JDK 1111.x服务器端主流 LTS,示例代码与语义无差异
JDK 1717.x新项目常用 LTS,适用于文中所有配置与示例
Guava Cache 核心 API23.x–32.x+CacheBuilder、LoadingCache、concurrencyLevel、refreshAfterWrite 语义稳定
并发分段实现(Segment/Striped)与 JDK ConcurrentHashMap 思路对齐通过 segmentFor(hash) 进行分段定位,减少锁竞争
自定义 LRU(LinkedHashMap)JDK 标准库基于 removeEldestEntry 的访问顺序 LRU,适合单线程或外层自行加锁场景

Guava Cache

并发设置

Guava Cache 通过设置 concurrencyLevel 参数来优化并发性能,使得缓存能够高效地支持多线程环境下的并发读写操作。以下是关于该机制的详细说明:

  1. 并发级别参数详解:

  2. 底层实现原理:

  3. 性能优化建议:

  4. 实际应用示例:

Cache<String, Object> cache = CacheBuilder.newBuilder().concurrencyLevel(8)  // 设置为8个分段.maximumSize(1000).build();
  1. 注意事项:
    • 该参数只在缓存构建时生效,创建后不可修改
    • 与maximumSize配合使用时,每个Segment会平均分配容量限制
    • 在极高并发场景下,可考虑结合refreshAfterWrite使用

这种设计使得Guava Cache在保持线程安全的同时,能够获得接近并发哈希表的性能表现,特别适合作为高性能应用中的本地缓存解决方案。

LoadingCache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(3)// 根据CPU情况进行并发.concurrencyLevel(Runtime.getRuntime().availableProcessors()).build(new CacheLoader<String, Object>() {@Overridepublic String load(String key) throws Exception {return "get: " + key;}});

concurrencyLevel = Segment 数组的长度,同 ConcurrentHashMap 类似 Guava Cache 的并发也是通过分离锁实现的:

@CanIgnoreReturnValue // TODO(b/27479612): consider removing this
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {int hash = hash(checkNotNull(key));return segmentFor(hash).get(key, hash, loader);}

LoadingCache 采用了类似 ConcurrentHashMap 的方式,将映射表分多个 Segment,Segment 之间可以并发访问,这样可以大大的提高并发效率,使得并发冲突的可能性降低了。

更新锁定

Guava Cache 提供了一个 refreshAfterWrite 定时刷新数据的配置项,这个特性主要用于保证缓存数据的时效性。当配置了 refreshAfterWrite 后,如果缓存项在指定时间内没有被更新或覆盖,则会在下一次获取该值的时候触发刷新机制。

刷新过程的具体实现如下:

  1. 后台会启动一个异步线程去回源(如数据库、远程接口等)获取最新数据
  2. 在刷新期间,所有对该缓存项的请求会被阻塞(block),默认阻塞时间为 1 分钟
  3. 刷新过程中只有一个请求会实际执行回源操作,避免了并发回源导致的系统压力
  4. 如果在阻塞时间内成功获取到新值,则返回新值并更新缓存
  5. 如果超过阻塞时间仍未获取到新值,则会返回旧值,保证系统不会因为刷新失败而不可用

典型应用场景包括:

  • 配置信息缓存(如系统参数、开关配置等)
  • 商品信息缓存(如价格、库存等)
  • 热点数据缓存(如排行榜数据)

示例配置代码:

CacheBuilder.newBuilder()
.refreshAfterWrite(5, TimeUnit.MINUTES)  // 5分钟未更新则触发刷新
.build(new CacheLoader<String, Object>() {@Overridepublic Object load(String key) throws Exception {return fetchDataFromDB(key);  // 数据加载逻辑}});

注意事项:

  1. 与 expireAfterWrite 不同,refreshAfterWrite 不会自动移除过期数据
  2. 建议设置合理的 refresh 时间,避免过于频繁的回源操作
  3. 阻塞时间可以通过重载 CacheLoader 的 reload 方法来自定义
LoadingCache<String, Object> cache = CacheBuilder.newBuilder().maximumSize(3)// 根据 CPU 数量并发.concurrencyLevel(Runtime.getRuntime().availableProcessors())// 3秒内阻塞的话会返回旧的数据.refreshAfterWrite(3, TimeUnit.SECONDS).build(new CacheLoader<String, Object>() {@Overridepublic String load(String key) throws Exception {return "get: " + key;}});

动态加载

动态加载行为是缓存系统中常见的机制,它通常发生在以下两种场景:

  1. 首次获取数据时缓存中不存在该数据
  2. 缓存中的数据已经过期(基于时间或大小的过期策略)

Guava Cache 采用了一种优雅的回调模式来实现动态加载。具体实现机制如下:

  1. 回调模式设计

    • 用户需要预先定义数据加载方式(Loader)
    • 当缓存需要加载新数据时,会自动回调这个预定义的加载方式
    • 这种设计遵循了"好莱坞原则"(不要调用我们,我们会调用你)
  2. 线程安全处理

    • 当多个线程同时请求同一个缺失的key时
    • Guava Cache 会确保只有一个线程执行加载操作
    • 其他线程会等待加载完成并共享结果
  3. 代码实现示例

// 获取对应哈希段的Segment对象
Segment<K, V> segment = segmentFor(hash);// 调用get方法,传入key、hash值和LoaderV value = segment.get(key, hash, loader);

其中关键组件说明:

  • loader:用户自定义的数据加载逻辑,需要实现CacheLoader接口
  • segmentFor(hash):Guava Cache的分段锁实现,用于提高并发性能
  • get()方法内部会先检查缓存,若不存在则调用loader加载数据

典型应用场景:

  1. 数据库查询缓存
  2. 耗时计算结果的缓存
  3. 远程服务调用结果的缓存

这种设计既保证了线程安全,又提供了良好的扩展性,让使用者可以专注于业务逻辑的实现。

自定义LRU

package icu.wzk;
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapCache<K, V> {private int limit;private LRUCache<K, V> internalCache;public LinkedHashMapCache(int limit) {this.limit = limit;this.internalCache = new LRUCache<>(limit);}public V get(K key) {return internalCache.get(key);}public void put(K key, V value) {this.internalCache.put(key, value);}public static void main(String[] args) {LinkedHashMapCache<String, String> cache = new LinkedHashMapCache<>(3);// 放入三个数据cache.put("1", "1");cache.put("2", "2");cache.put("3", "3");// 第四个数据cache.put("4", "4");for (Object o : cache.internalCache.values()) {System.out.println(o);}}}class LRUCache<K,V> extends LinkedHashMap<K, V> {private final int limit;public LRUCache(int limit) {super(limit, 0.75f, true);this.limit = limit;}@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {// 移除老的数据return size() > limit;}}

我们尝试运行,结果如下所示:
自定义实现LRU Cache

错误速查

症状根因定位修复
并发线程数上来后,缓存命中变差、Full GC 频繁concurrencyLevel 设置过大,Segment 数量过多导致元数据与锁开销放大检查 CacheBuilder.newBuilder() 中并发参数与实际 QPS、线程数将 concurrencyLevel 控制在预估并发线程数附近,避免盲目按 CPU×N 放大
以为配置了 refreshAfterWrite 后,过期数据会自动清除混淆 refreshAfterWrite(刷新)与 expireAfterWrite(过期剔除)通过日志或监控看到 key 长时间存在但有后台回源请求需要同时根据业务配置 expireAfterWrite 或主动 invalidate
高峰期 read 被卡住,线程堆栈停在 CacheLoader.load 附近load / 回源逻辑过慢,且 refreshAfterWrite 单 key 刷新会阻塞其他请求打线程 dump,观察大量线程阻塞在同一 key 的加载路径优化回源逻辑、增加超时与降级,必要时拆 key 或引入多级缓存
刷新期间期待“后台异步不阻塞”,实际请求却被挂起误解 refresh 语义:单 key 刷新期间,其他请求默认等待结果结合文档与代码调试发现 refresh 期间返回时间抖动根据业务接受度决定:改用 expire+懒加载,或对热点 key 单独设计缓存
多线程场景下自定义 LinkedHashMapCache 偶现数据错乱或 NPELinkedHashMap 非线程安全,外层未加锁就直接在并发环境下复用在压测或线上日志中发现 size 与实际访问不一致、偶发异常若需要并发,外层用 Collections.synchronizedMap 或 ReentrantLock 包裹,或直接改用 Guava Cache
maximumSize=3 等非常小,QPS 略高即频繁触发淘汰,命中率极差LRU 容量过小,未结合实际 key 数和访问分布评估 cache size监控中 eviction 数量远高于命中数根据热 key 数量与访问模式重新估算 maximumSize,适当上调缓存容量
使用 Runtime.getRuntime().availableProcessors() 直接套到并发CPU 核心数≠真实并发访问线程数,导致过高或过低的锁分段配置对比线程池大小、Tomcat 连接数与 CPU 核心数,发现不匹配以“高并发线程数”为主维度评估 concurrencyLevel,而不是机械等于 CPU 数

其他系列

AI篇持续更新中(长期更新)

AI炼丹日志-29 - 字节跳动 DeerFlow 深度研究框斜体样式架 私有部署 测试上手 架构研究,持续打造实用AI工具指南!
AI研究-132 Java 生态前沿 2025:Spring、Quarkus、GraalVM、CRaC 与云原生落地
AI模块直达链接

Java篇持续更新中(长期更新)

Java-180 Java 接入 FastDFS:自编译客户端与 Maven/Spring Boot 实战
MyBatis 已完结,Spring 已完结,Nginx已完结,Tomcat已完结,分布式服务已完结,Dubbo已完结,MySQL已完结,MongoDB已完结,Neo4j已完结,FastDFS 已完结,OSS正在更新… 深入浅出助你打牢基础!
Java模块直达链接

大数据板块已完成多项干货更新(300篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!
大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT案例 详解
大数据模块直达链接

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询