结论:@cacheable 原生注解不支持多级回源逻辑,因其底层 cachemanager 仅绑定单一缓存实现;必须手写 doublecache 类或封装统一访问入口,显式控制“先查 caffeine、未命中再查 redis、双未命中才查 db”的穿透流程,并严格遵循“先 db 后 redis 再本地”的更新顺序保障一致性。

直接说结论:不要用 @Cacheable 原生注解串联 Caffeine 和 Redis,它不支持多级回源逻辑;必须手写 DoubleCache 类或封装统一访问入口。
为什么 Spring Cache 抽象无法直接支持多级缓存
Spring 的 @Cacheable 底层依赖 CacheManager,而每个 CacheManager 只能绑定一种缓存实现(比如 CaffeineCacheManager 或 RedisCacheManager)。你无法让一次 @Cacheable 调用“先查 Caffeine,没命中再查 Redis”。强行配置两个 CacheManager 并指定不同名字,只会导致缓存操作被路由到单一实现,不会自动降级。
常见错误现象:
- 在方法上写
@Cacheable(cacheNames = "caffeineCache"),结果 Redis 里有数据但完全不查 - 用
@Caching组合多个@Cacheable,实际执行时只走第一个命中项,不构成“回源”逻辑 - 自定义
CacheManager返回包装类,但没重写get()方法的穿透逻辑,导致本地缓存未命中就直接返回 null
如何正确实现 DoubleCache 回源逻辑
核心是自己控制读取流程:先查本地 → 未命中查 Redis → Redis 命中则写回本地并返回 → 全未命中才查 DB。这个逻辑不能交给 Spring Cache 自动调度,必须显式编码。
实操建议:
- 定义一个
DoubleCache<k v></k>接口,包含get(K key, Supplier<v> loader)</v>方法,类似 Guava 的Cache.get(key, callable) - 实现类中调用
caffeineCache.getIfPresent(key),为空则调用redisTemplate.opsForValue().get(key) - Redis 查到后,立刻调用
caffeineCache.put(key, value)写入本地,再返回;注意设置合理的expireAfterWrite(如 10s),避免本地缓存长期 stale - loader 参数用于兜底查 DB,只在两级缓存都未命中时触发,且需保证线程安全(例如用
computeIfAbsent防止重复加载)
Caffeine 与 Redis 过期时间怎么配才不翻车
两级缓存过期策略不是越长越好,关键要错开、有层次。如果 Caffeine 过期时间比 Redis 还长,会出现“本地缓存一直返回脏数据,Redis 已更新但本地不感知”的一致性灾难。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
推荐配置组合:
- Caffeine:
expireAfterWrite(10, TimeUnit.SECONDS)+expireAfterAccess(30, TimeUnit.SECONDS)(双触发,兼顾写后及时性和访问热度) - Redis:
EXPIRE key 300(5 分钟),用redisTemplate.expire()显式设置,不要依赖@Cacheable的cacheManager默认 TTL - 绝对禁止:Caffeine 设置
expireAfterWrite(300, SECONDS)且 Redis 也设 300 秒 —— 此时本地缓存会卡住更新窗口,成为数据盲区
性能影响:Caffeine 过期太短(如 1s)会导致频繁回源 Redis;太长(如 5min)则放大不一致风险。10–30 秒是多数业务的平衡点。
更新/删除时如何同步两级缓存
读是“先 L1 后 L2”,写必须是“先 DB,再 L2,最后 L1”。顺序反了就会出现缓存和 DB 不一致。
关键动作清单:
- 更新数据库成功后,立即执行
redisTemplate.delete("user:123") - 紧接着调用
caffeineCache.invalidate("user:123")(注意不是put(null),invalidate 才真正清除) - 不要用“先删 Redis,再删本地”这种异步延迟清理方式 —— 中间窗口可能有请求进来,从 Redis 读到旧值再写回本地,污染整个本地缓存实例
- 如果用布隆过滤器防穿透,删除时也要同步更新布隆位图(如通过 RedisBloom 模块的
BF.MADD清理)
容易被忽略的一点:Caffeine 的 invalidateAll() 是全量清空,但它的内部是分段锁实现,高并发下可能引发短暂阻塞;生产环境慎用全量失效,优先按 key 精确删除。










