Spring Boot多级缓存一致性需靠DB驱动+Redis Pub/Sub广播+事务后回调清除本地缓存,@CacheEvict仅作用于当前JVM,无法跨节点同步Caffeine。

先更新DB再删Redis,但本地缓存怎么清?
本地缓存(如Caffeine)不共享,更新完Redis后,其他节点的本地缓存仍是旧值——这是多级缓存一致性的核心断点。不能靠@CacheEvict自动清除,因为Spring Cache的事件通知默认只作用于当前JVM实例。
必须主动广播失效信号。常见做法是:更新DB成功 → 删除Redis中对应key → 通过Redis Pub/Sub发送一条cache:invalidate:user:{id}消息 → 所有节点监听该频道并调用caffeineCache.invalidate(id)。
- 别用Topic泛播,按业务域+ID构造精确channel,避免全量刷新引发GC压力
- Pub/Sub不是强可靠,需加重试机制(比如失败时写入延迟队列)
- 若用Lettuce客户端,确保
setAutoReconnect(true)和setPingBeforeActivateConnection(true)开启,防止连接中断导致消息丢失
为什么@Cacheable + @CacheEvict在集群下会漏删本地缓存?
@Cacheable和@CacheEvict只管Spring Cache抽象层,对Caffeine实例本身无跨进程感知能力。你在节点A上执行@CacheEvict(key="#id"),只会清掉A本地的Caffeine和A连的Redis slot里的key,B节点的Caffeine完全不受影响。
现象就是:用户看到的数据一会儿新一会儿旧,尤其在滚动发布或灰度期间特别明显。
- 禁用
@CacheEvict做多级缓存驱逐,它只适合单机场景 - 把缓存操作拆成显式三步:
userMapper.updateById()→redisTemplate.delete("user:"+id)→redisTemplate.convertAndSend("cache:topic", id) - 监听端收到消息后,检查是否为本节点负责的业务ID范围(可配合ShardingSphere路由键做轻量过滤),再调用
caffeineCache.invalidate(id)
Redis集群中key哈希不一致导致双删失败
当使用user:{123}和user_profile:{123}两个key时,若没加花括号哈希标签,Redis Cluster可能把它们分到不同slot——删第一个key成功,删第二个失败或超时,造成L2残留。
更糟的是,Pub/Sub消息里只传了ID,监听端重建key时若格式不统一(比如有的用user:123,有的用user/{123}),会导致本地缓存清不干净。
- 所有涉及同一实体的key必须强制落在同一slot,统一用
user:{123}、user_profile:{123}写法 - 不要拼接字符串生成key,封装
CacheKeyGenerator类,所有key生成逻辑集中管理 - 在
delete前加redisTemplate.hasKey("user:{123}")探活,避免集群拓扑变更后命令发错节点
事务提交后删除缓存,但本地缓存清除时机容易错
很多人把caffeineCache.invalidate(id)写在service方法末尾,看似“更新完就清”,实则危险:事务还没提交,万一回滚,本地缓存已空,下次读就会从DB加载脏数据。
正确做法是绑定到事务生命周期,确保只有commit成功才触发清理。
- 用
TransactionSynchronizationManager.registerSynchronization()注册afterCommit回调 - 回调里只做轻量操作:发Pub/Sub消息,不直接操作Caffeine(避免事务上下文污染)
- 本地缓存清除动作必须放在消息消费者线程里,和数据库事务彻底解耦
真正难的不是写几行清除代码,而是让所有节点对“什么算最新数据”达成共识——这个共识必须由DB作为唯一事实源驱动,任何缓存层都不能擅自决定数据有效性。


















