Redis 缓存穿透场景怎样区分空值占位和真实缺失
我在排查缓存穿透时最容易踩的坑,是把 Redis 的“没有拿到业务数据”都当成同一种空结果。实际上,GET 返回 nil 可能意味着 Key 根本不存在;返回一个空值标记,则说明应用已经确认过“这个业务对象不存在”;返回普通字符串或 JSON,才是缓存命中的真实记录。三者混在一起,回源次数和监控结论都会失真。
官方地址:https://redis.io/docs/latest/
最稳妥的判断方式不是只看业务值是否为空,而是给缓存结果定义明确状态:hit表示有真实数据,negative表示空值占位,miss表示 Redis 中没有缓存。空值占位设置较短 TTL,真实缺失才回源;回源写回时使用带过期时间的写入策略。
先把三种缓存状态分开
Redis 的 Key 空间只知道一个 Key 是否存在,EXISTS 返回的是存在数量,并不会告诉应用“这个值是不是业务数据”。因此,EXISTS key = 1 不能直接等价于业务命中:它也可能只是一个代表“查过但不存在”的占位值。
我现在会把一次读取拆成下面三类结果:
| 状态 | Redis 表现 | 应用动作 |
|---|---|---|
hit | Key 存在,值能解析为真实记录 | 直接返回缓存数据,不查库 |
negative | Key 存在,值是约定的空值信封 | 返回业务层的“未找到”,不查库 |
miss | GET 返回 nil,Key 还没有缓存 | 查询数据源,再写回真实记录或空值信封 |

这里的关键是“空值占位仍然是缓存命中”。它只代表缓存层已经知道查询结果为空,并不代表数据库里有一条空记录。只要把这个语义写进状态字段,排查时就不会再用 value == nil 猜结果。
用状态信封保存结果
直接把特殊字符串当哨兵值虽然简单,但业务数据一旦可能出现同样的字符串,判断就会产生歧义。更稳的做法是给缓存值包一层轻量信封,让状态和数据分离。下面的 JSON 只是存储格式示意,字段名可以按项目命名约定调整:
{
"state": "hit",
"data": {"id": 42, "name": "示例对象"}
}
{
"state": "negative",
"data": null
}
读取时只接受三个状态:hit、negative 和未读到值时的 miss。未知状态不要默认为命中,也不要悄悄回源;可以把它记录为格式异常,再按照项目的兼容策略处理。
如果追求更小的载荷,也可以使用带前缀的字符串,例如 H|... 表示真实数据、N| 表示空值占位。无论选择 JSON 还是前缀,原则都是让“状态”成为显式协议,而不是把空字符串、零值和 nil 混为一谈。
给空值占位设置较短 TTL
空值占位的作用是挡住同一个不存在 Key 在短时间内重复回源,所以它需要过期时间;但它通常不应该和真实数据使用同一个 TTL。真实对象可能允许较长缓存,空值占位则应该更短,让后来创建的数据有机会被重新读取。
# 真实数据缓存时间较长,空值占位时间较短
SET user:42 '{"state":"hit","data":{"id":42}}' EX 600
SET user:4042 '{"state":"negative","data":null}' EX 60
# 用 TTL 判断占位是否仍在保护窗口内
TTL user:4042
SET 的 EX 选项会在写入时一起设置秒级过期时间;Redis 官方文档也说明成功的 SET 会处理原有 TTL,因此更新真实数据时要明确给出新的过期策略。排查时,Key 存在但 TTL 很短,通常更像空值占位;Key 不存在则是 miss,不能仅凭过期前后的业务结果倒推。
把回源写入收敛在缓存边界
真正让缓存穿透变严重的,通常不是一次 miss,而是大量并发请求同时发现 miss,然后一起查库。我的处理方式是让业务层只接收状态解析结果,把 Redis 读写、数据库回源和指标记录都集中在缓存边界内。

下面是一个精简的 Go 伪实现,重点是返回值的语义,不依赖某个具体 Redis 客户端版本。生产代码中应把序列化、超时和错误分类接到项目已有的基础设施上:
type cacheState string
const (
stateHit cacheState = "hit"
stateNegative cacheState = "negative"
stateMiss cacheState = "miss"
)
type envelope struct {
State cacheState `json:"state"`
Data json.RawMessage `json:"data,omitempty"`
}
func classify(raw string, found bool) (cacheState, []byte, error) {
// GET 没拿到 Key 才是 miss;Key 存在但 state=negative 是空值占位。
if !found {
return stateMiss, nil, nil
}
var item envelope
if err := json.Unmarshal([]byte(raw), &item); err != nil {
// 格式异常不能伪装成真实命中,交给上层做兼容或降级。
return "", nil, fmt.Errorf("decode cache envelope: %w", err)
}
switch item.State {
case stateHit:
return stateHit, item.Data, nil
case stateNegative:
return stateNegative, nil, nil
default:
// 未知状态保守处理,避免把占位或旧格式当成业务数据。
return "", nil, fmt.Errorf("unknown cache state %q", item.State)
}
}
回源完成后,真实记录和空值占位都要带 TTL 写回。对“只有在 Key 仍不存在时才写入”的场景,可用 SET key value EX seconds NX。官方文档定义了 NX 为仅在 Key 不存在时写入,这能减少较晚完成的请求覆盖已经写入的结果;它不是完整的分布式锁,也不替代业务层的并发控制。
# 仅当 Key 仍不存在时写入空值信封,避免覆盖较新的结果
SET user:4042 '{"state":"negative","data":null}' EX 60 NX
建立排查清单
遇到“缓存穿透仍然很高”时,我会按状态而不是按接口耗时排查:
- 先对同一个业务 Key 执行一次
GET,区分返回 nil、state=negative和state=hit。 - 再查看
TTL:没有 TTL 或 TTL 过长,说明空值占位策略可能没有落地;TTL 为负数时还要结合 Key 是否存在解释。 - 把指标拆成 cache_hit、cache_negative、cache_miss 和 origin_lookup,不要只保留一个 cache_miss 计数。
- 如果 miss 与 origin_lookup 同时陡增,检查回源写回是否失败;如果 negative 很高但业务刚创建数据,检查占位 TTL 是否过长。
- 最后检查 Key 规范化,确认读写两侧使用同一个前缀、业务标识和序列化协议。
这份清单的重点不是多执行几个 Redis 命令,而是把“没有真实数据”拆成可观测的状态。只有这样,空值占位是在保护数据库,还是把新数据挡住了,才有证据可判断。
几个容易混淆的问题
EXISTS 返回 1,是否说明缓存命中了?
不说明。它只说明 Key 存在;空值占位同样会让 EXISTS 返回 1。业务命中必须由缓存值的状态信封判断。
可以直接把空字符串当空值占位吗?
不建议。除非业务协议能严格保证真实数据永远不会是空字符串,否则空字符串无法表达“占位”与“真实值为空”的区别。使用保留状态字段更容易扩展和审计。
空值占位 TTL 越短越好吗?
不一定。太短会让不存在的 Key 频繁回源,太长会让后来创建的数据等待更久。应结合数据创建频率、回源成本和指标中的 negative/miss 比例调整。
结论
Redis 缓存穿透场景中,真正需要区分的是三种状态:Key 不存在是 miss,存在空值信封是 negative,存在可解析业务数据才是 hit。把状态写进缓存值、给空值占位设置短 TTL,并在回源写回时使用 SET EX NX,就能让业务判断、并发控制和监控指标落在同一套语义上。
Redis ROLE 怎样查看当前实例的复制角色
- 上一篇
- Redis ROLE 怎样查看当前实例的复制角色
- 下一篇
- Redis XADD MAXLEN 怎样控制 Stream 的近似长度
-
- 数据库 · Redis | 35分钟前 | Redis · 慢查询 · 日志管理 · 性能排查 · redis slowlog 慢日志 SLOWLOG GET SLOWLOG LEN slowlog-max-len
- Redis SLOWLOG 怎样读取并控制记录数量
- 219浏览 收藏
-
- 数据库 · Redis | 1小时前 | redis Lua 错误处理 脚本返回值 redis.call redis.pcall
- Redis Lua redis.pcall 遇到命令错误时怎样继续处理
- 391浏览 收藏
-
- 数据库 · Redis | 1小时前 |
- Redis Sorted Set 怎样按 score 做稳定分页
- 263浏览 收藏
-
- 数据库 · Redis | 1小时前 |
- RedisJSON JSON.OBJKEYS 怎样列出对象字段名
- 317浏览 收藏
-
- 数据库 · Redis | 1小时前 |
- Redis PFADD 批量写入 HyperLogLog 的去重计数方案
- 471浏览 收藏
-
- 数据库 · Redis | 16小时前 | Redis · redis AUTH ACL WHOAMI 当前用户 连接身份
- Redis ACL WHOAMI 怎样确认当前连接使用的用户
- 314浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 984次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 990次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1110次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 1080次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 925次使用
-
- Go与Redis实现分布式互斥锁和红锁
- 2022-12-22 117浏览
-
- Go+Redis实现延迟队列实操
- 2023-02-23 426浏览
-
- 一文搞懂Go语言操作Redis的方法
- 2023-01-07 171浏览
-
- Golang分布式应用之Redis示例详解
- 2023-01-07 113浏览
-
- Go Redis客户端使用的两种对比
- 2022-12-30 195浏览

