PubSub清空本地缓存易失效,因Redis不保证消息可达、无ACK、断连丢消息;需通过TTL兜底、主动校验、removal监听告警及正确invalidate调用保障可靠性。

直接用 Redis 的 PUB/SUB 通知所有节点清空 Caffeine 缓存,是最轻量、最低侵入的同步方式之一,但实际落地时很容易掉进「消息丢失」「重复消费」「本地缓存没清干净」这几个坑里。
为什么 PubSub 清空本地缓存容易失效?
Redis 的 PUB/SUB 是纯内存广播,不保证消息可达:订阅断开期间发布的消息彻底丢失;客户端重启后也不会重放;没有 ACK 机制,发了就完事。这意味着,只要某个服务实例短暂 GC、网络抖动或部署中,它就收不到那条 cache:invalidate:user:1001 消息,Caffeine 里的旧数据就会一直残留。
另外,Spring Boot 默认的 RedisMessageListenerContainer 在监听器抛异常时会静默跳过该消息,不会重试——你根本不知道哪条清空指令被吞了。
- 不要依赖单次
PUBLISH就认为本地缓存已同步 - 避免在监听器里做耗时操作(比如远程调用),否则阻塞线程池会导致后续消息积压甚至丢弃
- 别用通配符模式(
PSUBSCRIBE cache:*)监听大量 key,Redis PubSub 的 pattern match 性能随订阅数线性下降
怎么让 PubSub 清空真正可靠?
核心思路是「补漏 + 确认 + 降级」:不指望一次 pub 就搞定,而是用短期 TTL + 主动探测 + 最终兜底来覆盖各种异常场景。
- 发送清空指令时,除了
PUBLISH cache:invalidate:user:1001,顺带SET cache:invalidate:user:1001 "" EX 60,给一个 60 秒的兜底标记 - Caffeine 配置
expireAfterWrite(5, TimeUnit.MINUTES),哪怕某次清空失败,最多 5 分钟后也会自然过期 - 在 Caffeine 的
removalListener里记录日志,一旦发现是因「EXPIRED」或「REPLACED」被移除,说明清空指令可能没生效,可触发告警 - 关键业务读取前加一层校验:先查 Caffeine,命中后检查对应 key 在 Redis 中的版本号(如
user:1001:version),不一致则强制 reload 并清空本地
监听端代码必须避开的三个写法
很多项目直接在 MessageListener 里调 caffeineCache.invalidate(key),看似没问题,实则埋雷:
- 错误写法:
cache.invalidate(key.toString())—— 如果 key 是对象,toString()结果和 Caffeine 实际存的 key 不一致,清空失败 - 错误写法:
cache.asMap().remove(key)—— 绕过 Caffeine 的统计和监听机制,RemovalListener不触发,监控指标失真 - 错误写法:监听器方法上加
@Transactional—— PubSub 不在事务上下文里,这个注解完全无效,还可能引发 AOP 代理混乱
正确做法始终用 cache.invalidate(key),且确保传入的 key 类型和存入时完全一致(推荐统一用 String 或自定义 CacheKey 对象并重写 equals/hashCode)。
比 PubSub 更稳的替代方案其实就一个条件
如果你的 MySQL 已经接入了 Canel(或 Debezium、Shylock 等 CDC 工具),那优先走 binlog 解析同步。它天然具备「至少一次投递」「按事务顺序」「跨服务实例一致性」三大优势,而 PubSub 只是「尽力而为」。
PubSub 同步不是不能用,而是只适合对一致性要求宽松、服务实例数少(










