本地缓存应只存访问频次极高、更新频率极低、可容忍短暂不一致的稳定数据,如sku基础信息、灰度开关、角色权限白名单;避免缓存时效性状态或聚合结果,须设maximumsize硬上限并采用写驱动失效机制保障一致性。

本地缓存该放哪些数据?不是所有热 key 都适合下沉
本地缓存不是 Redis 的镜像,而是它的“热点过滤器”。真正该放进 Caffeine 的,只有那些:访问频次极高、更新频率极低、且能容忍短暂不一致的数据。比如商品详情页的 SKU 基础信息、配置中心的灰度开关、用户角色权限白名单——它们占全部缓存数据的 1%~5%,却扛下了 70% 以上的读请求。
常见错误是把所有 get 操作都套一层本地缓存,结果导致:OutOfMemoryError: Java heap space(堆内存爆掉)、缓存雪崩时本地和 Redis 同时失效、甚至因字段变更未同步引发业务逻辑错乱。
- ✅ 推荐策略:只缓存「稳定值」(immutable value),例如
product_name、category_id这类不随库存/价格实时变动的字段 - ❌ 避免缓存:含时效性状态的聚合结果,如
stock_available、user_last_login_time - ⚠️ 注意:Caffeine 默认使用堆内内存,
maximumSize必须设硬上限,建议按 JVM 堆的 5%~10% 预留,例如 4GB 堆配maximumSize(10_000)
怎么让本地缓存和 Redis 数据不“打架”?别信自动过期
靠 Caffeine 自带的 expireAfterWrite 或 expireAfterAccess 来“假装”一致性,是高并发下最常踩的坑。一旦本地缓存过期时间比 Redis 短,就会在窗口期内反复穿透到 Redis;如果更长,又会读到明显滞后的脏数据。
真正可靠的做法是「写驱动失效」:只要 Redis 被更新,就主动通知所有节点清掉本地副本。用 PUB/SUB 是目前最轻量、落地成本最低的方案。
- 发布端更新 Redis 后,立刻发一条消息:
redisTemplate.convertAndSend("cache-invalidate", "product:12345") - 订阅端监听频道,收到后执行:
caffeineCache.invalidate("product:12345") - ⚠️ 关键细节:消息体必须包含唯一业务标识(如
key),不能只发“刷新全部”,否则会误伤其他服务的本地缓存
为什么不用 Redis 的 keys * 扫描来批量清理?它根本跑不通
有人想绕过消息机制,直接在本地缓存失效时调用 KEYS product:* 获取所有相关 key,再逐个 invalidate。这个想法在单机开发环境看似可行,但上线就挂:
- ❌
KEYS是阻塞命令,在 Redis 6.0+ 默认被禁用,生产环境开启会拖慢整个实例 - ❌ 即使允许,扫描 10 万级 key 会耗尽连接池,触发
redis.clients.jedis.exceptions.JedisConnectionException - ✅ 正确替代:用
SCAN+ 游标分批(但依然不推荐用于失效场景),或更彻底地——从源头设计 key 命名规范,比如统一加前缀cache:product:,配合业务事件精准推送
Caffeine 的 refreshAfterWrite 和 expireAfterWrite 到底怎么选?
这两个参数名字像,行为差得远。expireAfterWrite 是“过期即删”,删完再查就得穿透;refreshAfterWrite 是“后台静默刷新”,命中时仍返回旧值,同时异步加载新值——这才是缓解淘汰压力的关键机制。
- ✅ 场景匹配:对允许短时 stale 的数据(如首页 Banner 配置),设
refreshAfterWrite(10, TimeUnit.MINUTES),既避免集中穿透,又保证 10 分钟内最终一致 - ⚠️ 注意:启用
refreshAfterWrite必须提供CacheLoader,且加载逻辑要防重入(比如加synchronized或分布式锁),否则可能并发触发多次 DB 查询 - ❌ 不要混用:同时设
expireAfterWrite和refreshAfterWrite会导致行为不可预测,Caffeine 文档明确说“二者互斥”
真正难的不是配参数,而是判断哪条数据值得进本地缓存、哪条必须直连 Redis。这个边界一旦划错,缓存就从减压阀变成放大器。










