当前位置:首页 > 文章列表 > 数据库 > Redis > Redis 缓存穿透场景怎样区分空值占位和真实缺失

Redis 缓存穿透场景怎样区分空值占位和真实缺失

来源:17golang原创 2026-11-11 20:47:27 0浏览 收藏

我在排查缓存穿透时最容易踩的坑,是把 Redis 的“没有拿到业务数据”都当成同一种空结果。实际上,GET 返回 nil 可能意味着 Key 根本不存在;返回一个空值标记,则说明应用已经确认过“这个业务对象不存在”;返回普通字符串或 JSON,才是缓存命中的真实记录。三者混在一起,回源次数和监控结论都会失真。

官方地址:https://redis.io/docs/latest/

最稳妥的判断方式不是只看业务值是否为空,而是给缓存结果定义明确状态:hit 表示有真实数据,negative 表示空值占位,miss 表示 Redis 中没有缓存。空值占位设置较短 TTL,真实缺失才回源;回源写回时使用带过期时间的写入策略。

先把三种缓存状态分开

Redis 的 Key 空间只知道一个 Key 是否存在,EXISTS 返回的是存在数量,并不会告诉应用“这个值是不是业务数据”。因此,EXISTS key = 1 不能直接等价于业务命中:它也可能只是一个代表“查过但不存在”的占位值。

我现在会把一次读取拆成下面三类结果:

状态Redis 表现应用动作
hitKey 存在,值能解析为真实记录直接返回缓存数据,不查库
negativeKey 存在,值是约定的空值信封返回业务层的“未找到”,不查库
missGET 返回 nil,Key 还没有缓存查询数据源,再写回真实记录或空值信封
Redis 请求键、缓存信封、hit 与 miss 状态以及 TTL 的静态关系说明图
图1:缓存信封把真实记录、空值占位和 TTL 放到同一条状态边界中;这是静态说明图,不是运行截图。

这里的关键是“空值占位仍然是缓存命中”。它只代表缓存层已经知道查询结果为空,并不代表数据库里有一条空记录。只要把这个语义写进状态字段,排查时就不会再用 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 读写、数据库回源和指标记录都集中在缓存边界内。

业务读取、Redis GET、空值占位、数据库查询和 SET EX NX 的缓存边界控制关系图
图2:缓存状态解析器连接 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

建立排查清单

遇到“缓存穿透仍然很高”时,我会按状态而不是按接口耗时排查:

  1. 先对同一个业务 Key 执行一次 GET,区分返回 nil、state=negative 和 state=hit。
  2. 再查看 TTL:没有 TTL 或 TTL 过长,说明空值占位策略可能没有落地;TTL 为负数时还要结合 Key 是否存在解释。
  3. 把指标拆成 cache_hit、cache_negative、cache_miss 和 origin_lookup,不要只保留一个 cache_miss 计数。
  4. 如果 miss 与 origin_lookup 同时陡增,检查回源写回是否失败;如果 negative 很高但业务刚创建数据,检查占位 TTL 是否过长。
  5. 最后检查 Key 规范化,确认读写两侧使用同一个前缀、业务标识和序列化协议。

这份清单的重点不是多执行几个 Redis 命令,而是把“没有真实数据”拆成可观测的状态。只有这样,空值占位是在保护数据库,还是把新数据挡住了,才有证据可判断。

几个容易混淆的问题

EXISTS 返回 1,是否说明缓存命中了?

不说明。它只说明 Key 存在;空值占位同样会让 EXISTS 返回 1。业务命中必须由缓存值的状态信封判断。

可以直接把空字符串当空值占位吗?

不建议。除非业务协议能严格保证真实数据永远不会是空字符串,否则空字符串无法表达“占位”与“真实值为空”的区别。使用保留状态字段更容易扩展和审计。

空值占位 TTL 越短越好吗?

不一定。太短会让不存在的 Key 频繁回源,太长会让后来创建的数据等待更久。应结合数据创建频率、回源成本和指标中的 negative/miss 比例调整。

结论

Redis 缓存穿透场景中,真正需要区分的是三种状态:Key 不存在是 miss,存在空值信封是 negative,存在可解析业务数据才是 hit。把状态写进缓存值、给空值占位设置短 TTL,并在回源写回时使用 SET EX NX,就能让业务判断、并发控制和监控指标落在同一套语义上。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Redis ROLE 怎样查看当前实例的复制角色Redis ROLE 怎样查看当前实例的复制角色
上一篇
Redis ROLE 怎样查看当前实例的复制角色
Redis XADD MAXLEN 怎样控制 Stream 的近似长度
下一篇
Redis XADD MAXLEN 怎样控制 Stream 的近似长度
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    984次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    990次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1110次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1080次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    925次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码