
本文详解如何通过重写 CacheLoader.reload() 方法,结合 refreshAfterWrite 与 asyncReloading,在 Guava Cache 中实现真正的非阻塞读取——请求直接返回旧值,后台异步加载新数据,避免 DB 查询阻塞主线程。
本文详解如何通过重写 `cacheloader.reload()` 方法,结合 `refreshafterwrite` 与 `asyncreloading`,在 guava cache 中实现真正的非阻塞读取——请求直接返回旧值,后台异步加载新数据,避免 db 查询阻塞主线程。
Guava Cache 原生的 refreshAfterWrite 并不自动提供非阻塞语义:当条目触发刷新条件后,首次 get() 调用仍会同步执行 reload()(默认委托给 load()),导致线程阻塞。要实现“立即返回旧值 + 后台异步更新”,关键在于显式重写 CacheLoader.reload(K, V) 方法,并确保其返回 ListenableFuture
以下是正确实现步骤:
✅ 正确做法:重写 reload() 方法(核心)
import com.google.common.cache.{CacheLoader, LoadingCache}
import com.google.common.util.concurrent.{ListenableFuture, MoreExecutors, Futures}
import scala.concurrent.ExecutionContext
class AsyncDbCacheLoader[K, V](dbExecutor: ExecutionContext) extends CacheLoader[K, V] {
// 必须重写 reload:返回 Future,且不阻塞调用线程
override def reload(key: K, oldValue: V): ListenableFuture[V] = {
// 在独立线程池中异步加载新值
val future = scala.concurrent.Future {
// ⚠️ 这里执行实际的 DB 查询(耗时操作)
loadFromDatabase(key)
}(dbExecutor)
// 将 Scala Future 转为 Guava ListenableFuture
Futures.transform(future, (v: V) => v, MoreExecutors.directExecutor())
}
// load() 仅用于初始加载(首次 get),可同步实现
override def load(key: K): V = {
loadFromDatabase(key)
}
private def loadFromDatabase(key: K): V = {
// 模拟 DB 查询逻辑(如 JDBC/ORM 调用)
???
}
}
✅ 构建缓存:启用 refresh + 禁用自动 asyncReloading
val dbThreadPool = Executors.newFixedThreadPool(8) val dbEc = ExecutionContext.fromExecutor(dbThreadPool) val cacheLoader = new AsyncDbCacheLoader[String, User](dbEc) val cache: LoadingCache[String, User] = CacheBuilder.newBuilder() .refreshAfterWrite(30, TimeUnit.SECONDS) // 触发后台刷新的阈值 .expireAfterWrite(5, TimeUnit.MINUTES) // 硬过期兜底(防止 stale 数据永久存在) .concurrencyLevel(4) .recordStats() .build(cacheLoader) // ❌ 不要再 wrap with CacheLoader.asyncReloading(...)
? 关键点:不要使用 CacheLoader.asyncReloading()。该工具仅对 load() 做异步包装,但 refreshAfterWrite 机制优先调用 reload();若 reload() 未重写,它将回退到同步 load(),彻底失效。
✅ 使用方式:get() 完全非阻塞
// 立即返回当前缓存值(即使已过 refresh 时间),绝不阻塞
val user: User = cache.get("user-123")
// 后台已在 reload() 中发起异步 DB 查询,成功后自动更新缓存
// 下次 get() 将拿到新值(或仍为旧值,直到 reload 完成)
⚠️ 注意事项与最佳实践
- reload() 必须返回 ListenableFuture:这是 Guava 判断是否支持异步刷新的唯一依据。返回 null 或抛异常会导致刷新失败并降级为同步加载。
- 线程安全:reload() 可被并发调用(同一 key 多次 get 触发),需确保 DB 加载逻辑线程安全,或依赖数据库连接池/ORM 自身并发控制。
- 错误处理:建议在 reload() 中捕获 DB 异常并记录日志,避免抛出异常(否则 Guava 会保留旧值并静默失败);可通过 Futures.withFallback 提供降级逻辑。
- 监控与调试:启用 .recordStats() 后,检查 cache.stats().evictionCount() 和 cache.stats().loadExceptionCount() 可验证刷新行为是否符合预期。
- 替代方案考虑:若项目已迁移到 Java 11+,推荐评估 Caffeine —— 其 refreshAfterWrite 默认异步且 API 更简洁,Guava 已停止积极维护。
通过精准重写 reload(),你就能获得真正低延迟、高吞吐的缓存读取体验:用户请求毫秒级响应,数据新鲜度由后台刷新保障,彻底解耦 I/O 与业务逻辑。











