西西软件园多重安全检测下载网站、值得信赖的软件下载站!
软件
软件
文章
搜索

首页 → 业内动态 → 业内资讯 → 加了Redis缓存数据库还是被打挂:穿透、击穿、雪崩的区别在这

加了Redis缓存数据库还是被打挂:穿透、击穿、雪崩的区别在这

相关软件相关文章发表评论 来源:本站整理时间:2026/11/2 10:16:23字体大小:A-A+

作者:xiix点击:1次评论:0次标签: Redis 缓存穿透 缓存击穿 缓存雪崩 缓存一致性

  • 类型:编程辅助大小:2.8M语言:中文 评分:10.0
  • 标签:
立即下载

给站点加缓存这件事,几乎每个做站的人都干过。改一行代码查一次 Redis,接口耗时从八百毫秒掉到二十毫秒,页面的响应速度立刻变得顺滑,监控面板上的数据库 QPS 曲线也跟着塌下去一块。第一次看到这种曲线的人都会觉得,这事到此就结束了。

然后某天流量稍微涨了一点,或者某个运营活动一推,数据库连接池瞬间打满,接口大面积超时,CPU 跑到一百,让人抓狂。查一圈下来才发现,缓存虽然在,但缓存没派上用场请求大量绕过缓存直接压到数据库。这个场景在业内的说法叫缓存的「三大经典问题」:缓存穿透、缓存击穿、缓存雪崩。这三个词看起来像,其实成因、表现和药方都不太一样,很多人搞混了以后乱加锁,反而把机器拖得更慢。这篇就按我实际排查的顺序,把这三种情况拆开讲。

如果你的站已经上了缓存但数据库压力还是很大,或者正打算给一台机器配一套缓存方案,可以先看看这两家的机器做参考:RackNerd(全球VPS,特价常推)、狗云DOGYUN(高性价比云服务器,多机房可选),2 核 4G 的配置跑中小站点加一层 Redis 缓存完全够用,内存余量留给缓存和数据库的 buffer pool 反而比硬堆 CPU 更实在。

一、缓存穿透:查的根本不存在的数据

先说穿透。它指的是一个压根不存在的 key,请求进来缓存里查不到,代码就往数据库查,数据库也查不到,返回空。听起来这没什么查不到不是正好少一次查询吗?问题在于,如果有人拿不存在的用户 ID 循环刷你的接口,比如 id=999999、id=999998 这种,缓存永远不会命中,数据库每次都得真的去查一次、真的去走一遍索引树,然后真的返回一个「查不到」。

所以穿透的本质是:缓存对「不存在的值」没有任何保护作用,空结果没有写回去。判断起来其实不难,看两个指标就够了:一是数据库的查询 QPS 明显高于业务正常流量,二是这些查询的耗时几乎都一样、走的都是同一条 SQL、参数只在小范围里变动。如果是这种形态,八成就是穿透,而不是雪崩或者击穿。

解法有两种思路。一种是空值缓存:查不到的时候也往 Redis 里塞一个空标记,比如 key 设为 user:999999,value 设成 0,加个短一点的过期时间,比如六十秒。这样同一个不存在的 ID 在这一分钟内只会打一次数据库,攻击成本直接被摊薄了。另一种是布隆过滤器,在缓存之前先拦一道,把确定不存在的 ID 挡掉。这两种不冲突,小站点用空值缓存基本就够用了,没必要上布隆过滤器增加复杂度。另外该加的限流还是要加,接口层做个 QPS 阈值,超过就直接返回,别让所有压力都走到数据库连接池那一层。

Redis缓存服务器与缓存节点示意图

二、缓存击穿:一个热点 key 过期瞬间被所有人同时打穿

击穿和穿透只有一字之差,成因完全不同。击穿针对的是一个真实存在、而且极其热门的 key。比如首页 banner、商品详情页的爆款商品、某个正在被疯抢的秒杀商品,它的缓存值在 Redis 里只存一份。

假设这个 key 的过期时间设的是 300 秒。到了第 300 秒那一刻,这个 key 就没了。就在这一瞬间,假设有一千个请求同时打进来,一千个请求全都缓存未命中,全都去查数据库,全部试图去重建这个缓存值。这一千次查询是同一份数据的一千次重复劳动。数据库没有压力才怪。

击穿的特征特别好认:平时一切正常,只有某一个时间点数据库出现一个短暂的尖峰,尖峰持续时间很短,几秒到几十秒就过去了,然后自动恢复。而且事后去查缓存,会发现这个 key 又好好地躺在那里了,只是它的过期时间刚好是尖峰发生的时间附近。

常用的解法有三个,按推荐程度排。第一是给热点 key 加互斥锁:第一个没命中的请求去抢锁,抢到的那个去查数据库并回填,没抢到的请求先睡个几十毫秒再重试一次缓存。这时候总共只有一次数据库查询,其余请求都在等。这是开销最小、效果最直接的办法。第二是给热点 key 设置不过期,或者设一个特别长的过期时间,靠后台脚本主动更新。这种方式要接受「缓存和数据库可能短暂不一致」的现实,适合那些更新频率低但访问极高的数据。第三是逻辑过期,也就是在值里额外存一个逻辑上的过期时间戳,物理上不删,读取时发现逻辑上过期了,就返回一个旧值同时异步去更新,牺牲一点点数据新鲜度换取永不击穿。秒杀这种极端场景一般用第一种或者第三种。

缓存击穿时热点数据被并发请求同时访问示意图

三、缓存雪崩:大批 key 同时失效,或者缓存服务直接挂了

雪崩是最容易造成严重事故的一种。它的第一种成因是「同时过期」:很多人在给一批数据批量设置 TTL 时,顺手全写了同一个值,比如全部 3600 秒。那这些 key 就会在同一时刻集体失效,几万个请求同时穿透到数据库,数据库直接被压垮,然后缓存里的数据也来不及重建,雪上加霜。

第二种成因更干脆:Redis 本身挂了。网络抖动、内存爆掉、机器被服务商重启、主从切换失败不管是哪种,缓存不可用的时候,如果没有降级策略,全部流量会瞬间转向数据库。对一台 4 核 8G 的机器来说,数据库就是它身上最重的部件,扛不住这种冲击。

雪崩的解法核心是「别让它们一起过期」和「缓存挂了要能活」。前者是给 TTL 加随机值,别再整整齐齐 3600 了,改成 3600 加上 0 到 600 之间的随机数,把失效时间摊开,这一招几乎零成本,效果立竿见影。后者是设置合理的连接超时和重试次数,同时在代码里加一层兜底:缓存读失败或者读到空的时候,不要无限重试,转而走一个降级逻辑,比如返回一份兜底数据、或者返回上一页的静态快照、再或者干脆返回一个友好的错误页,同时把请求降级到数据库的速率限制住。另外多部署一个 Redis 实例做主从或者哨兵,比单机裸跑要稳得多。顺带说一句,Redis 内存吃满被踢出键这件事也会造成类似雪崩的效果,所以 maxmemory 和淘汰策略要提前设好,别等出事再调。

Redis缓存过期与内存淘汰策略示意图

四、缓存和数据库的一致性,别追求绝对实时

上面三种问题解决完,还有一个绕不开的话题:缓存里的数据和数据库对不上怎么办。我的看法是,绝大部分业务场景不需要强一致。常见的策略是「先更新数据库,再删除缓存」,也就是 Cache Aside 模式。更新的时候先把数据库改了,再把对应的缓存 key 删掉,下次读的时候缓存未命中,从数据库读出新值再回填。这个顺序很重要如果先删缓存再更新数据库,中间那个时间窗里另一个读请求会把旧值重新写回缓存,脏数据就长期留下了。

但即便是「先库后删」,也不是万无一失的。删除缓存这一步可能失败,消息也可能丢。稳妥一点的做法是把「删除缓存」这个动作通过消息队列异步做一遍,加上重试和失败告警。也可以考虑延迟双删:更新库之后先删一次缓存,隔几百毫秒再删一次,用来覆盖主从延迟导致的旧值回写窗口。

还有一个现实的问题就是缓存里的数据过期得比数据库更新慢,导致用户短时间内看到旧数据。对绝大多数展示类场景这是可以接受的,页面顶多显示一下更新时间。真要做强一致的话,代价会明显上一个台阶,得引入分布式锁或者更复杂的版本号机制,通常不划算。我自己的取舍是:能用延迟双删解决的就别上锁,能异步解决的就别同步,能容忍几秒旧数据就别追求实时。

数据库与缓存数据同步一致性流程示意图

五、怎么在本地快速验证这几件事

光看理论没用,动手敲一遍才有感觉。我一般会用几件趁手的工具来搭这个小实验:用 Redis 安装配置工具 在本地或自己的服务器上一分钟起一个实例,然后用 redis-cli 或者可视化客户端连上去观察命令行返回的统计数字。关键是盯这几个指标:keyspace_hits 和 keyspace_misses,命中率高说明缓存真在工作;expired_keys 突然暴涨,就是雪崩的信号;instantaneous_ops_per_sec 飙高或者出现毛刺,往往对应击穿。

复现击穿的方式最简单:用一个脚本并发请求同一个 key,第一次先设个很短的 TTL,比如五秒,然后看着这五秒过去的那一瞬间数据库日志里刷出来的一堆重复查询。再把 TTL 设长一点,同样操作,毛刺就消失了。穿透也很好造,循环请求一个不存在的 ID,看数据库的查询数是不是跟着请求数一起涨。这两个实验各花不到十分钟,但会让你对这三类问题的理解完全不一样。

六、最后几个容易忽略的细节

收个尾,说几个我踩过的坑。

第一,缓存 key 的命名一定要规范。用 `user:123` 还是 `user_123` 还是 `UserInfo-123` 都行,但必须全项目统一,并且最好带上业务前缀,不同业务混在一起后期排查会非常痛苦。

第二,缓存里不要放大对象。有些人图省事直接 `SET key <整个用户对象>`,一个用户对象几 KB,十万个键就是几百 MB,内存一涨就触发淘汰,性能断崖式下跌。更好的做法是只存 ID,详情回源查;或者拆成哈希分开存,需要哪部分取哪部分。

第三,删除大 key 要小心。`DEL` 一个几 MB 的 value 会阻塞主线程几秒,在生产环境是事故的常见来源。用 `UNLINK` 代替 `DEL`,让后台线程慢慢释放;或者用 `SCAN` 一点点分批清理。这个坑我是真踩过,一个 `DEL` 卡住整个 Redis,后来那一整天的请求全乱了。

第四,监控要提前埋。缓存命中率、慢查询日志、连接池等待时间、Redis 内存使用率,这四个指标配上告警,比事后翻日志快太多了。

总的来说,缓存穿透是没找到的请求反复打穿,缓存击穿是一个热点 key 瞬间被集火,缓存雪崩是大批 key 同时失效或者服务整体不可用。搞混了以后用错解法,反而会引入新的问题比如给穿透场景加分布式锁,锁越多数据库压力越大。工具装好,环境搭起来,把这三种场景各复现一遍,剩下的就是照着上面的思路一条条对号入座了。

    相关评论

    阅读本文后您有什么感想? 已有人给出评价!

    • 8 喜欢喜欢
    • 3 顶顶
    • 1 难过难过
    • 5 囧囧
    • 3 围观围观
    • 2 无聊无聊

    发表评论

    昵称:
    表情: 高兴 可 汗 我不要 害羞 好 下下下 送花 屎 亲亲