空值标记不能存 null,因Spring Data Redis序列化器对null处理不一致,易导致反序列化异常或误判;MQ消息须带上下文与版本,避免重复写入空值,且需网关拦截非法参数、消费者幂等、定期清理过期空key。

空值标记不能存 null,消息队列也不能直接“刷新空 key”——这两步不配对,缓存穿透就还在漏。
为什么 redisTemplate.opsForValue().set("user:999999", null, 120, TimeUnit.SECONDS) 是错的
Spring Data Redis 的默认序列化器对 null 处理不一致:有的版本存进去是 Redis 的 nil,取出来却变成字符串 "null";有的反序列化直接抛 SerializationException。业务代码里写 if (cached == null) 会误判,甚至把 "null" 当成有效数据返回给前端。
- 统一用明确标记值,比如
"__NULL__"(字符串)或new byte[0](空字节数组) - 读取后必须显式判断:
if ("__NULL__".equals(cached)) { return null; } - 如果用 Jackson 序列化器,建议存结构化空响应,如
{"code":404,"msg":"not_found"},避免反序列化失败
消息队列不能只发 “刷新 user:999999”,得带上下文和版本
空值缓存本身只是兜底,它不解决“谁该重建”“重建什么”“重建是否过时”的问题。如果 MQ 消息只含 key 名,消费者查 DB 发现数据仍不存在,就会反复写入同样的 "__NULL__",浪费资源还掩盖真实问题。
- 消息体必须包含原始请求参数(如
userId=999999)、触发时间戳、以及可选的业务上下文(如来源渠道、风控等级) - 写入缓存前加
SETNX product_999999:version <ts>或用 Lua 脚本保证“仅首个成功消费者执行重建” - 若 DB 确认无此记录,应跳过写缓存,或写入带 TTL 的空标记(此时 TTL 仍按 60s~180s 设,不能沿用原消息里的过期逻辑)
空值缓存 + MQ 的组合必须绕开三个典型陷阱
这套方案看似完整,但落地时最容易在边界处失效:不是技术没跑通,而是条件没对齐。
- 接口层未做参数校验:攻击者传
id=abc或id=-1,请求根本不需要进缓存层,更不该发 MQ —— 这类请求应在网关或 Controller 层直接拦截并返回 400 - MQ 消费者没做幂等:同一条“修复 user:999999”的消息被重投两次,会导致两次查 DB、两次写
"__NULL__",Redis 内存虚高且无意义 - 空 key 清理滞后:大量过期空值 key 不会立刻从内存释放(Redis 惰性删除),需配合定期扫描
SCAN+TTL非正数的 key 做主动清理,否则INFO memory中used_memory持续偏高
空值标记和 MQ 的协同,本质是“先稳住流量,再择机修复”,而不是让 MQ 替代校验或代替缓存设计。真正难的从来不是怎么发消息,而是怎么定义“值得修复的空值”——这需要和业务 SLA、数据写入节奏、风控策略一起对齐,而不是孤立配置一个 TTL 或塞一条消息。


















