Redis缓存三大坑:穿透、击穿、雪崩,一次讲透

简介: 缓存穿透、击穿、雪崩,名字像兄弟但成因解法完全不同。本文深入讲解三种问题的原理、实现细节与隐藏的坑,覆盖布隆过滤器、互斥锁、逻辑过期、过期随机化等解法。

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!


缓存是扛高并发的利器,但缓存不是"上了就稳了"。用不好,三个经典问题轮流轰炸你:穿透、击穿、雪崩

这仨名字听着像兄弟,但成因、危害、解法完全不同,甚至解法之间还有互相踩坑的地方。今天往深里讲。


一、缓存穿透:查一个根本不存在的东西

场景:用户查一个 id=99999 的商品,这个 id 数据库里根本没有。缓存里自然也没有,于是每次请求都"穿透"缓存,直接打到数据库。数据库查了一圈,返回空。

危害:正常的空查询本身不致命,但攻击者会利用这一点——用大量不存在的 id 疯狂请求,每次请求都绕过缓存直达数据库,直接把数据库打挂。

解法一:布隆过滤器

在缓存前面加一层过滤器,快速判断"这个 key 一定不存在"。

布隆过滤器的原理是一个 bit 数组 + 多个 hash 函数。写入时,对 key 做 k 次 hash,把对应位置的 bit 置 1;查询时,只要有一个位置是 0,就说明这个 key 一定不存在。

它的特性是:"不存在"是确定的,"存在"可能误判。所以它天然适合拦截非法 id——宁可偶尔放过一个不存在的 id 去打数据库,也不能误杀正常请求。

两个关键参数要算对:

  • bit 数组大小 m:根据预估数据量 n 和目标误判率 p,公式是 m = -n * ln(p) / (ln2)^2
  • hash 函数个数 kk = m/n * ln2

举个例子:预估 1000 万个 key,误判率控制在 1%,bit 数组大约需要 9.6MB,hash 函数取 7 个。参数没算对,要么误判率飙升,要么内存浪费。

工程上直接用 Guava 的 BloomFilter 或 Redisson 的 RBloomFilter,别自己手写 hash。

解法二:缓存空值

查不到也往缓存里塞一个 null,设个短过期时间(比如 60 秒)。下次同样的请求直接命中缓存的 null,不再打数据库。

这里有个隐蔽的坑:如果攻击者用海量不同的非法 id,每个 id 都缓存一个 null,会把缓存内存塞满。所以空值的过期时间一定要短,而且要限制数量。

解法三:参数校验

明显非法的请求(id 为负数、超范围、格式不对)在入口直接拦截。这层是成本最低的防御,但只能挡住"一眼假"的请求,挡不住"看起来合法但不存在"的 id。

三个解法不是三选一,而是分层配合:参数校验挡最外层,布隆过滤器挡中间层,缓存空值兜底。


二、缓存击穿:热点 key 过期的瞬间

场景:某个热点 key(比如秒杀商品的库存、首页的热门内容)在过期的一瞬间,大量请求同时涌进来,发现缓存没了,全部打到数据库。数据库瞬间被压垮。

和穿透的区别:穿透是"查不存在的数据",击穿是"查存在但刚好过期了的热点数据"。

解法一:互斥锁(Mutex)

缓存未命中时,只有一个请求能拿到锁去数据库查询并重建缓存,其他请求等待。

这里最容易写错的是忘了双重检查。正确的流程是:

1. 查缓存,命中 → 直接返回
2. 未命中 → 尝试获取锁
3. 拿到锁 → 再查一次缓存(可能别的线程已经重建好了)
4. 还是没有 → 查数据库 → 写缓存 → 释放锁
5. 没拿到锁 → sleep 重试,回到第 1 步

第 3 步的"再查一次缓存"是关键,省掉它,高并发下会重复查库。锁的粒度也要小,用分布式锁(Redisson)时要注意锁的过期时间设短一点,防止线程挂掉后锁不释放。

解法二:逻辑过期

给热点 key 不设 Redis 的物理过期时间,而是在 value 里存一个"逻辑过期时间戳",比如 {data: "...", expireAt: 1712345678}

读取时判断时间戳,没过期直接返回;发现过期了,先返回旧值(保证用户体验),同时启动一个后台线程去查数据库更新缓存。核心思想是"先返回、后更新",不让用户在缓存重建期间等待。

逻辑过期适合"热点明确、能容忍短暂旧数据"的场景,比如首页 banner、排行榜。

解法三:提前预热

在热点 key 过期前主动刷新。可以在代码里做定时任务,或者在 key 快过期时触发异步刷新。预热解决的是"过期瞬间"的问题,但前提是你知道哪些 key 是热点。


三、缓存雪崩:大量 key 同时失效

场景:两种典型情况——要么是大量 key 在同一时间过期,要么是 Redis 直接宕机。无论哪种,一瞬间所有请求都打到数据库。

危害:这是三个问题里最严重的,击穿是"一个点",雪崩是"一整片"。

解法一:过期时间加随机值

别让所有 key 在同一时刻过期。给每个 key 的过期时间加一个随机偏移,比如基础 5 分钟 + 随机 0-300 秒,把过期时间打散到 5-10 分钟之间。

这是成本最低、收益最高的一招,很多雪崩就死在"所有 key 用同一个过期时间"。

解法二:多级缓存

本地缓存(Caffeine)+ Redis 两级。Redis 挂了,本地缓存还能顶一阵,给恢复争取时间。

但要注意两级缓存的失效顺序:更新数据时,先更新 Redis,再更新本地缓存;本地缓存的过期时间要短于 Redis,避免本地缓存里的旧数据活太久。

解法三:Redis 高可用

单点 Redis 是雪崩的最大诱因。方案有两个层次:

  • 主从 + 哨兵:主节点挂了,哨兵自动把从节点提升为主,秒级切换
  • Redis Cluster:数据分片到多个节点,单个节点挂掉只影响一部分数据,配合副本还能继续服务

能上 Cluster 就别只用主从,主从切换期间的短暂不可用,在极端流量下也可能引发雪崩。

解法四:限流降级

数据库入口做限流,超过阈值直接熔断。宁可部分用户看到降级提示,也不要让全库被压垮。Sentinel 或 Hystrix 都能做。


四、三兄弟对比表

问题 成因 关键特征 核心解法
穿透 查不存在的数据 每次都打到 DB 布隆过滤器 / 缓存空值 / 参数校验
击穿 热点 key 过期瞬间 单点瞬间流量洪峰 互斥锁 / 逻辑过期 / 预热
雪崩 大量 key 同时失效 / Redis 宕机 整片缓存失效 过期随机化 / 多级缓存 / 高可用 / 限流

五、一个真实的生产事故

曾经有个电商系统,首页 banner 的缓存过期时间统一设成了 2 小时。运维在凌晨统一上线,结果凌晨 2 点整,所有 banner 缓存同时过期,流量全部打到数据库。数据库连接池瞬间打满,整个首页白屏。

最后只改了一件事:给每个 key 的过期时间加随机偏移。一行代码,问题彻底消失。

缓存问题往往不是技术多难,而是没想到那个"同时"。


缓存是放大器——用对了放大性能,用错了放大故障。穿透、击穿、雪崩的区别和解法,加上每一招背后的实现细节和隐藏的坑,是用好 Redis 的必修课。别等数据库被打挂了才想起来补。

小耶在手,SQL不愁。

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

相关文章
|
1月前
|
存储 关系型数据库 MySQL
读写混合TPS差六倍,PostgreSQL与MySQL架构差异实测
从架构设计、索引实现、事务隔离、复制机制、运维体验五个维度深度对比PostgreSQL与MySQL,覆盖MySQL 9.0向量检索与PostgreSQL 17新特性,附权威基准数据和选型决策框架
|
1月前
|
缓存 监控 NoSQL
命中率98%跌至23%,17条告警齐发:Redis缓存三大故障复盘
从618促销缓存雪崩事故切入,深度解析缓存穿透、击穿、雪崩的底层机制、生产级防御方案与监控告警策略,附布隆过滤器实现和分布式锁代码
|
1月前
|
存储 固态存储 关系型数据库
从月账单5万到3.5万:云数据库成本优化的完整复盘
上云本应是降本增效,但很多企业上云之后,账单反而越滚越大。实例规格买高了、历史数据堆在SSD上、测试环境没人关、过期快照没清理——每一笔费用都在悄悄累积。本文从云账单的三大“黑洞”出发,拆解云成本失控的根因,给出实例降配、冷热数据分层、僵尸资源清理三条可落地的优化路径,帮助DBA和运维工程师用数据驱动成本优化,让每一分钱都花在刀刃上。
|
1月前
|
SQL 运维 监控
慢查询日志的“高级用法”:从找慢SQL到做容量规划
慢查询日志是DBA最熟悉的工具,但大多数人只用它来找“跑得慢的SQL”。如果只做到这一步,你只用了慢查询日志20%的价值——剩下的80%是建立性能基线、预测容量瓶颈、评估优化效果、发现潜在风险。本文从慢查询日志的进阶用法出发,讲解如何通过持续记录慢查询建立性能基线、如何通过慢查询趋势预测容量瓶颈、如何将慢查询日志从“故障排查工具”升级为“容量规划工具”,帮助读者从“出了问题再查”升级到“看着趋势主动调整”。
|
1月前
|
存储 搜索推荐 关系型数据库
纯向量库架构上线两周出事故,我帮他们重构后发现了3个选型误区
从一次生产事故出发,拆解向量数据库爆火的真实原因,深入底层索引机制和架构取舍,分析融合趋势。给从业者一个清醒的判断框架。
|
1月前
|
SQL JSON 算法
SQL执行计划的“成本模型”:读懂cost,理解优化器为什么选这个计划
EXPLAIN能告诉你优化器选了哪个执行计划,但说不出它为什么这么选——明明有索引它却走全表扫描,明明A计划更快它却选了B计划。优化器不靠猜,它靠一套成本模型(Cost Model)做决策。本文从优化器的成本模型出发,拆解cost的构成(IO_cost、CPU_cost、memory_cost),讲解如何通过EXPLAIN FORMAT=JSON和OPTIMIZER_TRACE看到优化器的“思考过程”,并通过真实案例展示优化器“算错账”的根因,帮助读者从“知道选了谁”升级到“理解为什么选它”。
|
1月前
|
存储 固态存储 关系型数据库
DBA凌晨查账单:每月5万的云数据库竟有一半在空转,我的六个优化动作和数据验证
从一次真实的云数据库成本优化复盘出发,分享实例规格合理选型、冷热数据分层、存储压缩、弹性伸缩策略、清理历史数据、预留实例规划六个关键步骤,附优化前后的成本对比数据和操作要点。
|
1月前
|
存储 关系型数据库 MySQL
查询从45秒降到0.3秒,存储从1.2TB缩到180GB:IoT时序数据选型复盘
5万台IoT设备日增4.3亿行数据,MySQL三天崩溃的完整复盘。从写入模型、B+树瓶颈、Gorilla压缩原理对比时序库与关系型数据库的根本差异,含宽窄表重构SQL、冷热分离迁移策略、time_bucket查询优化,以及3条实战避坑经验。
|
1月前
|
SQL JSON 移动开发
SQL派生表优化实战:从物化机制到LATERAL JOIN的完整进阶
很多人只知道“子查询改JOIN就快了”,但不知道为什么,也不知道什么时候该改、什么时候不该改。本文从派生表的物化机制出发,拆解临时表膨胀、索引失效的根因,通过真实案例对比派生表、CTE、LATERAL JOIN三种写法的性能差异,帮助读者从“知道现象”升级到“理解原理”。
|
1月前
|
SQL 关系型数据库 MySQL
UNION vs UNION ALL:一个“ALL”字,性能差了一个数量级
UNION和UNION ALL的区别很多人知道,但INTERSECT和EXCEPT的执行机制、性能差异,以及如何用JOIN和子查询替代,很多人并不清楚。本文从集合操作的执行计划出发,拆解UNION去重的“隐形代价”、INTERSECT与INNER JOIN的本质差异、EXCEPT与NOT EXISTS的性能对比,并通过真实案例展示集合操作在业务场景中的正确用法与避坑指南,帮助读者从“会写集合操作”升级到“理解集合操作的底层逻辑”。

热门文章

最新文章