在千万级 QPS 的高并发系统中,缓存(Cache)是提升性能的银弹。然而,当流量洪峰真正到来时,缓存系统的任何一个微小漏洞都可能导致数据库瞬间崩溃。
作为后端工程师,我们不仅要会用 Redis,更要懂得如何设计一个“打不穿”的缓存架构。本文将系统讲解“缓存穿透、击穿、雪崩”的本质区别,并从工程落地角度给出解决方案。
一、概念辨析:三座大山
很多开发者容易混淆这三个概念,我们用一个“防弹衣”的生动比喻来区分它们:数据库(DB)是人,缓存(Cache)是穿在人身上的防弹衣。
1. 缓存穿透
- 现象:子弹打在了防弹衣没有覆盖的地方,直接伤到了人。
- 定义:请求的数据在缓存中不存在,在数据库中也不存在。
- 后果:流量直接打穿缓存和 DB,通常由恶意攻击(如查询 ID=-1)或代码 Bug 导致。
2. 缓存击穿
- 现象:防弹衣完好,但某一个点被机枪持续扫射,最终被烫穿。
- 定义:某个极度热点 Key(如秒杀商品)在失效的瞬间,海量并发请求同时涌入。
- 后果:DB 瞬间压力过载,常见于电商大促、微博热搜。
3. 缓存雪崩
- 现象:由于不可抗力,防弹衣整个脱落了。
- 定义:海量 Key 在同一时间集中过期,或缓存节点宕机。
- 后果:原本由缓存承担的流量全部转移到 DB,导致 DB 宕机。
核心差异对比表
| 特性 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
|---|---|---|---|
| 数据状态 | DB 和 Cache 都没有 | DB 有,Cache 刚好失效 | DB 有,Cache 大面积失效 |
| Key 的特征 | 很多非法的 Key | 少数几个热点 Key | 海量的 Key |
| 流量特征 | 恶意流量或代码 Bug | 高并发读 | 正常流量的瞬间转移 |
二、解决方案:构建防御纵深
要解决这三个问题,不能指望单一的技术手段,而需要构建多层防御体系。
1. 防御穿透:布隆过滤器 (Bloom Filter)
针对缓存穿透,最有效的手段是在访问缓存之前,先校验 Key 是否可能存在。
- 机制:利用位数组和多次 Hash,判断 Key 是否存在。
- 特点:说“不存在”则一定不存在(100% 准确);说“存在”则可能不存在(有误判率)。
- 误判原因(为什么不准?):“位置共享”。你查询的 k 个位置全是 1,但这可能是其他几个元素凑巧填满的,不是你留下的。
- 进阶:使用布谷鸟过滤器 (Cuckoo Filter),相比布隆过滤器,它支持删除操作,且空间利用率更高。
Java 实战 (Redisson)
// 依赖:org.redisson:redisson
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("idBlockList");
// 初始化:预计 1000 万数据,误差率 1%
bloomFilter.tryInit(10000000L, 0.01);
// 业务使用
if (!bloomFilter.contains("user_123")) {
return null; // 直接拦截,不查 Redis 也不查 DB
}
2. 防御击穿:请求合并 (SingleFlight)
对于热点 Key 失效导致的击穿,核心思路是:哪怕有一万个并发请求,只允许一个去查数据库,其他人等待结果共享。
在 Go 语言中,singleflight 是处理此类问题的神器。
Go 实战 (SingleFlight)
import "golang.org/x/sync/singleflight"
var g singleflight.Group
func GetProduct(id string) (string, error) {
// Do 方法确保针对同一个 id,同时只有一个执行逻辑在跑
v, err, _ := g.Do(id, func() (interface{}, error) {
// 只有第一个到达的请求会执行这里(查 DB)
return db.Query(id)
})
if err != nil {
return "", err
}
return v.(string), nil
}
3. 防御雪崩:随机过期与多级缓存
- 随机 TTL:在设置过期时间时,加上一个随机值(如 1-5 分钟),避免集体失效。
- 多级缓存:L1 本地缓存(Guava)+ L2 分布式缓存(Redis)。即使 Redis 挂了,本地缓存还能抗住短时间的热点流量。
三、工程化:监控与预警
没有监控的系统就是在“裸奔”。要预防这些问题,必须建立完善的指标监控(Metrics)。
- 缓存命中率 (Cache Hit Rate)
- 异常:如果命中率从 99% 骤降到 60%,往往是雪崩的前兆。
- 空查次数 (DB Null Count)
- 异常:如果 DB 返回 Null 的次数激增,说明遭到了缓存穿透攻击。
- 回源 QPS (Back-to-Source QPS)
- 异常:缓存层流量平稳,但 DB 流量暴涨,需排查是否有大 Key 过期。