☰
Redis 从 0 到 1(十一):缓存穿透/击穿/雪崩三连击,Go 3 套防御代码
2026/10/17 2:38:15 网站建设 项目流程

我不会起名字322· 后端 / 算法 / 数据库

📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL

文章目录

  • Redis 从 0 到 1(十一):缓存穿透/击穿/雪崩三连击,Go 3 套防御代码
    • 一、三个事故,三种机理
    • 二、Go 实操 1:布隆过滤器挡穿透(收藏点 1)
    • 三、Go 实操 2:互斥重建挡击穿(收藏点 2)
    • 四、Go 实操 3:随机 TTL 防雪崩(一行的事)
    • 五、压测对比(收藏点 3)
    • 六、排查步骤:缓存相关数据库打满从哪查
    • 七、总结

Redis 从 0 到 1(十一):缓存穿透/击穿/雪崩三连击,Go 3 套防御代码

上一篇《Redis 从 0 到 1(十):16384 个槽怎么分?6 节点 Cluster 与 Go 重定向处理》我们打完了部署篇的最后一块拼图,链接:https://blog.csdn.net/2501_92769340/article/details/167399592 。
本篇进入实战弧线,第一题就是 Redis 面试和线上事故的双料冠军:缓存穿透、击穿、雪崩。

一、三个事故,三种机理

它们名字像,其实是完全不同的病:

问题事故形态根因
缓存穿透攻击者用-1、null之类的 key 狂刷接口,数据库被拖垮key 本身不存在,缓存永远不命中,每次请求都落到数据库
缓存击穿某热 key 过期瞬间,1000 个并发同时查库重建,数据库瞬时被打爆高并发 + 重建成本高 + 没有互斥
缓存雪崩半夜一批 key 同时过期(比如批量导入时同 TTL),大量请求同时打到数据库过期时间集中 + 无降级

对策也一一对应:穿透用布隆过滤器把"不存在的 key"挡在缓存层之前;击穿过期的瞬间只允许一个请求去重建;雪崩把过期时间打散。

二、Go 实操 1:布隆过滤器挡穿透(收藏点 1)

思路:查询前先问过滤器——“这个 key 可能存在吗?”。答案"一定不存在"就直接返回,根本不去碰缓存和数据库;答案"可能存在"才走正常缓存流程。过滤器用 Redis 的 Bitmap 实现(SETBIT/GETBIT),不引入 RedisBloom 模块,只要 String 类型支持即可。

packagebloomimport("context""hash/crc32""hash/fnv""github.com/redis/go-redis/v9")// 双哈希函数定位两个 bit 位,误判率约 1e-4 量级(够用即可,别追求精确)funcoffsets(keystring)(uint32,uint32){h1:=fnv.New32a();h1.Write([]byte(key))h2:=crc32.ChecksumIEEE([]byte(key))size:=uint32(1<<22)// 4M bit ≈ 512KB 一个位图returnh1.Sum32()%size,h2.Sum32()%size}// Add 把 key 加进过滤器(数据加载时同步调用)funcAdd(ctx context.Context,rdb*redis.Client,keystring)error{a,b:=offsets(key)bitKey:="bloom:users"pipe:=rdb.Pipeline()pipe.SetBit(ctx,bitKey,int64(a),1)pipe.SetBit(ctx,bitKey,int64(b),1)_,err:=pipe.Exec(ctx)returnerr}// MightExist 一定不存在返回 false;可能存在返回 true(有误判,无漏判)funcMightExist(ctx context.Context,rdb*redis.Client,keystring)(bool,error){a,b:=offsets(key)// 两个 bit 都为 1 才认为可能存在(Pipeline 一次 RTT 查两个位)pipe:=rdb.Pipeline()va:=pipe.GetBit(ctx,"bloom:users",int64(a))vb:=pipe.GetBit(ctx,"bloom:users",int64(b))if_,err:=pipe.Exec(ctx);err!=nil{returnfalse,err}returnva.Val()==1&&vb.Val()==1,nil}

查询入口改造:

funcGetUser(ctx context.Context,rdb*redis.Client,idstring)(string,error){ok,err:=bloom.MightExist(ctx,rdb,"user:"+id)iferr!=nil{return"",err}if!ok{return"",nil// 布隆说一定不存在:直接空结果,不碰数据库}val,err:=rdb.Get(ctx,"user:"+id).Result()// ... 未命中再回源,回源为空时写短 TTL 空值缓存(见下)_=valreturnval,err}

两级防线配合使用更稳:布隆挡掉"绝对不存在的 key",对漏过的攻击再补"空值缓存"(数据库查不到也缓存 30 秒,key 命中率立刻归零)。

三、Go 实操 2:互斥重建挡击穿(收藏点 2)

击穿的本质是"1000 个人同时发现缓存没了,一起去查库"。解法:只允许第一个人去查,其他人等结果。

import"golang.org/x/sync/singleflight"vargroup singleflight.GroupfuncGetProduct(ctx context.Context,rdb*redis.Client,skustring)(string,error){// singleflight:同一 sku 的并发查询自动合并,只有一个 goroutine 真正执行val,err,_:=group.Do("product:"+sku,func()(any,error){cached,err:=rdb.Get(ctx,"product:"+sku).Result()iferr==nil{returncached,nil}if!errors.Is(err,redis.Nil){returnnil,err}// 双重检查:拿到互斥锁确认自己重建(防止 singleflight 合并前已有人重建)ok,err:=rdb.SetNX(ctx,"lock:product:"+sku,"1",5*time.Second).Result()iferr!=nil{returnnil,err}if!ok{time.Sleep(50*time.Millisecond)// 等持锁者写完,再读一次缓存returnrdb.Get(ctx,"product:"+sku).Result()}deferrdb.Del(ctx,"lock:product:"+sku)// 回源数据库(省略),写入缓存,返回return"",nil})iferr!=nil{return"",err}returnval.(string),nil}

注意点:singleflight 合并的是进程内并发;跨实例部署时,SetNX 互斥锁是第二道保险(第 13 篇把它升级成完整分布式锁)。

四、Go 实操 3:随机 TTL 防雪崩(一行的事)

// 基础 TTL 30 分钟,加 ±5 分钟随机抖动,避免整批 key 同时到期ttl:=30*time.Minute+time.Duration(rand.Int63n(10*60))*time.Second-5*time.Minute rdb.SetEx(ctx,key,val,ttl)

同时给"批量导入"场景立规矩:导入 100 万个 key 时,TTL 用base + i*offset错开,而不是统一 24 小时。

五、压测对比(收藏点 3)

模拟:热 key 过期瞬间 500 并发查询(数据库回源模拟为 100ms),对比三种状态:

防御方案数据库瞬时 QPS接口 P99
无防御500(全穿透)100ms+ 排队
互斥重建≈1105ms
互斥重建 + 降级空值≈1,异常时 05ms(降级)

雪崩场景:5000 个 key 同刻过期(无抖动)vs TTL 抖动,数据库请求峰值从 5000 QPS 降到 <50。

六、排查步骤:缓存相关数据库打满从哪查

  1. 看数据库慢查询:是否大量相同 SQL 查不存在的 id → 穿透;
  2. 看缓存命中率曲线:从 95% 跳水 → 大面积失效(雪崩);
  3. 看单 key 访问热度:某个 key QPS 异常高且刚过期 → 击穿;
  4. 对症下药:穿透加布隆 + 空值缓存;击穿加互斥重建;雪崩加 TTL 抖动 + 多级缓存。

七、总结

  1. 三个问题三种病:穿透是"查不存在的",击穿是"热 key 过期瞬间",雪崩是"一起过期";
  2. 防御三板斧:布隆过滤器 + 空值缓存、singleflight + SetNX 互斥重建、随机 TTL 抖动;
  3. 过滤器有不存在的误判但没有漏判,空值缓存兜住误判,两级配合最稳;
  4. 所有防御代码都是幂等的,故障时可以随时降级。

下一篇预告:缓存挡住了恶意流量,但缓存和数据库之间还有一笔糊涂账——先删缓存还是先更新库?下一篇《先删缓存还是先更新库?4 种方案的 Go 并发实测》,我们用 100 个 goroutine 把四种方案的脏读概率打出来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询