结论:不能直接用@Cacheable实现两级缓存,因其仅绑定单个CacheManager,无法自动降级;必须手写DoubleCache显式控制“本地→Redis→DB”回源流程,并错开TTL、统一JSON序列化保障一致性。

直接上结论:两级缓存不是“加了就快”,而是必须让 Caffeine 拦住 80% 以上热点请求,同时用 Redis 承担跨实例共享和兜底职责——否则反而增加复杂度、引入一致性风险。
为什么不能直接用 @Cacheable 套两层?
Spring 的 @Cacheable 默认只绑定一个 CacheManager,强行配置两个(比如 CaffeineCacheManager 和 RedisCacheManager)会导致注解无法自动选择层级,最终要么全走本地、要么全走 Redis,失去“两级”意义。
- 常见错误现象:
Cacheable方法命中后返回 null 或 ClassCastException,本质是序列化不匹配(Caffeine 存对象,Redis 存 JSON 字符串) - 真实使用场景:需要显式控制访问路径,比如商品详情页的
getProduct(Long id)方法,必须先查本地、再查 Redis、最后查 DB - 正确做法:放弃全自动注解,改用
caffeineCache.get(key, loader)+ 手动调用redisTemplate.opsForValue().get()组合
如何配置 Caffeine 和 Redis 的序列化保持兼容?
这是最隐蔽也最容易线上翻车的一环:Caffeine 缓存的是原始 Java 对象,而 Redis 默认用 JdkSerializationRedisSerializer 存二进制,一旦类结构微调或部署环境类加载器不同,就会在反序列化时抛 ClassNotFoundException 或 InvalidClassException。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 推荐方案:Caffeine 层保持 Java 原生(零开销),Redis 层统一用
GenericJackson2JsonRedisSerializer - 关键配置点:
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()),且确保实体类有无参构造、字段可 public 或带 getter/setter - 性能影响:JSON 序列化比 JDK 序列化慢约 15%,但换来的是可读性、跨语言兼容性和上线稳定性
- 容易踩的坑:没配
setHashKeySerializer和setHashValueSerializer,导致 Hash 结构操作失败;或者误用StringRedisTemplate处理对象
过期时间怎么设才不互相打架?
本地缓存过期太短,频繁穿透到 Redis;过期太长,又导致数据陈旧。两级缓存的 TTL 必须错开,且遵循“L1
- 典型参数组合:
Caffeine配expireAfterWrite(2, TimeUnit.MINUTES),Redis配SETEX key 300 value(5 分钟) - 为什么不是倍数关系?因为要预留“缓存失效窗口”:当 Caffeine 过期瞬间,多个线程可能同时触发回源,此时 Redis 若也刚好过期,就会集体打穿 DB(即缓存雪崩)
- 进阶技巧:对超高频数据(如首页 Banner),Caffeine 可设
refreshAfterWrite(30, TimeUnit.SECONDS)实现异步刷新,避免阻塞请求 - 注意点:Redis 的过期是服务端行为,Caffeine 的过期是客户端行为,两者无法强同步,只能靠时间差+业务容忍度来平衡
缓存更新和删除时,两级怎么协同?
写操作永远比读操作危险。很多团队只做“查询双写”,却忽略“更新/删除”时的缓存清理逻辑,结果出现“Redis 删了,Caffeine 还留着”的脏数据。
- 安全模式:所有写操作(
update/delete)必须显式调用caffeineCache.invalidate(key)+redisTemplate.delete(key) - 不要依赖“被动过期”:Caffeine 的
expireAfterAccess在低频访问下会让脏数据驻留很久;Redis 的过期也无法覆盖所有更新场景 - 分布式锁不是银弹:如果用 Redis 分布式锁保证删除顺序,会拖慢写性能;更轻量的做法是加一层“缓存版本号”,读时校验,写时递增
- 最容易被忽略的地方:批量操作(如
deleteByIds(List<Long>))必须遍历每个 key 清理两级缓存,不能只删 Redis 后就认为完事
真正难的从来不是把 Caffeine 和 Redis 接在一起,而是让它们在高并发、多实例、频繁变更的业务里,既快又准地协作——这要求你对每一处 get、put、invalidate 调用都清楚它在什么条件下生效、在什么路径下失效、以及失效后谁来兜底。

















