guava cache 不适合多级缓存强一致性场景,因其 refreshafterwrite 同步阻塞、无失败重试、无并发控制;pub/sub 仅能广播失效信号,无法解决回源击穿与消息丢失问题;应改用 caffeine。

Redis 的发布订阅(Pub/Sub)确实能用于多实例间同步本地缓存,但**Guava Cache 不适合用在需要强一致性的多级缓存场景中**——这不是 Pub/Sub 能解决的问题,而是 Guava 本身的设计缺陷导致的。
下面直说关键点:怎么做、为什么、容易踩什么坑。
Guava Cache 的 refreshAfterWrite 是同步阻塞的,别用它做多级缓存刷新
Guava 的 refreshAfterWrite 在触发 reload 时会阻塞当前请求线程,直到 load() 返回。如果后端是 Redis 或 DB,网络延迟一高,整个 HTTP 请求就被卡住。更糟的是,它不提供失败重试或降级逻辑,一次超时就直接返回旧值,还可能永远不更新。
而 expireAfterWrite 虽然强制过期,但过期后多个并发请求仍会同时调用 load() ——这就是典型的缓存击穿,Guava 自身不加锁,也无去重机制。
- Guava 不支持像 Caffeine 那样的异步刷新 + 同步锁组合
- Guava 初始化后无法动态改 TTL,运行时没法根据 Redis 状态调整
- Guava 的 maximumSize 淘汰只看 entry 数量,大对象容易引发 OOM,不能当内存兜底用
Pub/Sub 只能广播“失效信号”,不能替代缓存本身的刷新策略
你可以在 Redis 中 PUBLISH user_update user_123,其他节点监听到后调用 guavaCache.invalidate("user_123")。但这只是清掉本地缓存,下一次读请求才会触发 load() 回源。
问题来了:如果此时 Redis 也刚好过期或不可用,load() 就得直连 DB ——而 Guava 默认不防并发,N 个请求同时打 DB,雪崩风险极高。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- Pub/Sub 消息不保证送达(Redis 重启、订阅连接断开就丢)
- 没有 ACK 机制,发了就不管,失败无感知
- 消息内容只能是 key,没法带新值,必须靠本地 reload,加重回源压力
本地缓存该用 Caffeine,不是 Guava
所有近期生产实践(2026 年内主流框架和性能报告)都指向一个结论:Caffeine 是当前 JVM 本地缓存的事实标准。它原生支持:
-
refreshAfterWrite是异步非阻塞的,不影响主线程响应 - 内置
executor可控刷新线程池,避免线程爆炸 - 支持
weigher基于内存大小淘汰,防 OOM - 提供
asMap().computeIfAbsent等原子操作,可配合分布式锁做安全加载
如果你硬要用 Guava,至少做到:expireAfterWrite 设置为 Redis TTL 的 0.3~0.5 倍(比如 Redis 设 30 分钟,Guava 设 10 分钟),且绝对禁止 maximumSize,并自行包装 load() 方法加分布式锁(如 RedisLock)防止击穿。
Pub/Sub 的实际使用必须配重连 + 信道隔离
Java 客户端用 Jedis 或 Lettuce 时,subscribe 是阻塞命令,必须跑在独立线程里;一旦连接断开,默认不会自动重连。
- 订阅线程要捕获
JedisConnectionException,sleep 后重试 connect + subscribe - 不同业务域用不同 channel,比如
user:invalidate、config:reload,别全扔进cache_update - 发布方要用
publisherJedis.publish(channel, key),不要传 JSON 字符串——解析开销白费,key 足够定位 - 监听到消息后,先校验
nodeId是否等于自己,避免处理本节点刚发出去的消息(自循环)










