expireAfterWrite 适用于时效性强的数据(如实时行情),从写入起倒计时;expireAfterAccess 适用于读多写少场景(如配置信息),从最后一次读或写起计时,二者不可共存。

Guava Cache 的 expireAfterWrite 和 expireAfterAccess 到底怎么选
写缓存过期逻辑时,expireAfterWrite 是最常用也最容易误用的——它从写入那一刻起倒计时,不管后续有没有读。而 expireAfterAccess 是“最后一次读或写之后”开始计时,适合会频繁读但更新不频繁的场景(比如用户配置、权限信息)。
常见错误现象:expireAfterWrite 配了 10 分钟,但业务每 2 分钟就 put 一次,结果缓存永远不淘汰;反过来,如果只读不写,expireAfterAccess 才能真正触发清理。
-
expireAfterWrite更适合「时效强」的数据,比如实时行情、临时令牌 -
expireAfterAccess更适合「读多写少」的数据,比如字典表、静态配置 - 两者不能同时设置,Guava 会抛
IllegalStateException - 注意:过期是惰性检测,只有在
get或缓存维护线程扫描时才真正移除,不是准时销毁
设置最大容量后,Guava Cache 怎么决定淘汰谁
用 maximumSize 限制缓存条目数时,Guava 默认采用 LRU(最近最少使用)策略淘汰——不是按过期时间,也不是按写入顺序,而是看最近一次 get 或 put 的时间戳。
容易踩的坑:refreshAfterWrite 不影响淘汰逻辑,它只控制后台异步刷新;而 maximumSize 和 weigher 不能共存,否则启动就报 IllegalArgumentException。
立即学习“Java免费学习笔记(深入)”;
- 如果要按内存大小而非条目数限制,必须用
maximumWeight+weigher,比如给每个 value 算字节数 - LRU 在高并发
get场景下可能引发竞争,但 Guava 内部已分段加锁,一般不用干预 - 淘汰不触发
RemovalListener的EXPIRED类型,而是SIZE类型,注意区分回调原因
为什么 CacheBuilder.newBuilder().build() 不能直接用 asMap() 操作
调用 cache.asMap() 返回的是一个视图(view),不是普通 HashMap。它支持 get、containsKey,但禁止 put、remove 等结构性修改,否则抛 UnsupportedOperationException。
真实使用场景:调试时想遍历当前缓存内容,或者做条件性清理(比如清除所有 key 匹配某前缀的项),只能通过 asMap().keySet().removeIf(...) 这类间接方式。
-
asMap()的entrySet()和values()也是只读视图 - 想手动触发清理,应该用
cache.invalidate(key)或cache.invalidateAll(keys) - 如果需要全量扫描+过滤,优先考虑
cache.asMap().forEach(...),避免先转 list 再遍历,省对象创建
Guava Cache 在 Spring Boot 中自动装配时的常见陷阱
Spring Boot 2.7+ 默认不再自动配置 Guava Cache,如果你依赖 @EnableCaching + @Cacheable,底层默认走的是 ConcurrentMapCacheManager,不是 Guava。
要真正用上 Guava,必须显式定义 CacheManager bean,并指定 com.google.common.cache.Cache 实例。否则 @Cacheable 看似生效,实际走的是无过期、无容量限制的简单 map。
- 别漏掉
@Bean方法返回类型必须是CacheManager,且内部包装的是GuavaCache(不是原始com.google.common.cache.Cache) - Spring 的
GuavaCache包装器不支持refreshAfterWrite,只支持expireAfterWrite和maximumSize - 配置类里别写错包名:
com.google.common.cache.CacheBuilder,不是org.springframework.cache.guava(那个是旧版废弃路径)
expireAfterAccess 的“访问”到底算什么、asMap() 的只读边界、还有 Spring 集成时的隐式 fallback,这几个地方一旦忽略,问题往往在线上压测或低峰期才暴露。










