4. 缓存穿透问题排查:定义与成因、支付场景下的穿透案例、布隆过滤器原理与配置
4.1 缓存穿透的定义与核心成因
缓存穿透是指查询一个根本不存在的数据时,请求绕过缓存直接打到数据库的现象。由于缓存中不存在该键,每次请求都会穿透到存储层,导致数据库压力骤增,甚至引发雪崩。
| 维度 | 说明 |
|---|---|
| 定义 | 查询一个缓存和数据库中都不存在的 key,每次请求都穿透到 DB |
| 核心成因 | 业务层未对不存在的数据做特殊处理;恶意攻击者利用不存在的 key 发起高频查询 |
| 影响范围 | 数据库连接池耗尽、响应超时、服务降级甚至宕机 |
| 与缓存击穿区别 | 击穿是热点 key 失效;穿透是 key 本身不存在 |
关键认知:缓存穿透不是缓存容量问题,而是数据不存在导致的请求绕过问题。在支付订单场景中,攻击者常利用随机生成的、不存在的订单号进行扫描。
4.2 支付场景下的穿透案例:恶意查询不存在的订单号
4.2.1 场景描述
某电商支付系统,订单号采用 18 位数字(如 202503211234567890)。攻击者编写脚本,批量请求 /order/status?orderId=202503219999999999 这类不存在的订单号。由于缓存中无此 key,每次请求都查询 MySQL 订单表,导致数据库 CPU 飙升到 100%,正常用户支付回调超时。
4.2.2 攻击流量特征
- 请求量:每秒 5000+ QPS,持续 3 分钟
- key 分布:订单号连续递增但均不存在
- 缓存命中率:从 95% 骤降至 5%
- 数据库慢查询:出现大量
SELECT * FROM orders WHERE order_id = ?且耗时 > 2s
4.2.3 排查步骤
- 监控告警:Redis 缓存命中率曲线断崖式下跌,DB 连接数飙升
- 日志分析:发现大量请求的 orderId 不在最近 24 小时订单范围内
- 流量回放:提取 1000 条异常请求,逐一查询数据库确认均不存在
- 确认穿透:缓存中无 key,DB 中无记录,符合穿透定义
实战教训:该案例中,团队最初误判为“缓存击穿”,尝试用互斥锁重建缓存,反而加剧了数据库压力。正确做法是识别出“数据不存在”这一本质,采用布隆过滤器或空值缓存。
4.3 布隆过滤器原理与配置
4.3.1 布隆过滤器核心原理
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它由一个位数组和多个哈希函数组成。
- 添加元素:将元素通过 k 个哈希函数映射到位数组的 k 个位置,将这些位置置为 1
- 查询元素:同样计算 k 个哈希位置,如果所有位都是 1,则元素可能存在;如果任意一位为 0,则元素一定不存在
- 误判率:存在一定的假阳性概率(判断存在但实际不存在),但不会漏判(判断不存在则一定不存在)
支付场景适配:布隆过滤器非常适合拦截“不存在的订单号”。因为订单号一旦生成就固定不变,我们可以将所有合法订单号预加载到过滤器中,查询前先检查,如果过滤器说“不存在”,直接拒绝,不再查询缓存和数据库。
4.3.2 布隆过滤器参数配置
| 参数 | 含义 | 推荐值(支付订单场景) |
|---|---|---|
| n | 预期元素数量(订单总数) | 1000 万(根据业务量调整) |
| p | 期望误判率 | 1%(0.01) |
| m | 位数组大小(bit 数) | ≈ -n * ln(p) / (ln2)^2 ≈ 9585 万 bit ≈ 11.4 MB |
| k | 哈希函数个数 | ≈ (m/n) * ln2 ≈ 7 |
4.3.3 Redis 中配置布隆过滤器
Redis 4.0+ 通过 RedisBloom 模块支持布隆过滤器。安装后使用以下命令:
# 1. 创建布隆过滤器:key=order_bloom,误判率=0.01,容量=10000000
BF.RESERVE order_bloom 0.01 10000000
# 2. 批量添加合法订单号(生产环境通过脚本批量加载)
BF.MADD order_bloom 202503210000000001 202503210000000002 202503210000000003
# 3. 查询订单号是否存在
BF.EXISTS order_bloom 202503219999999999
# 返回 0 → 一定不存在,直接拒绝
BF.EXISTS order_bloom 202503210000000001
# 返回 1 → 可能存在,继续查缓存/DB
4.3.4 布隆过滤器 vs 空值缓存方案
| 对比维度 | 布隆过滤器 | 空值缓存(缓存 null) |
|---|---|---|
| 内存占用 | 极低(千万级数据约 11 MB) | 较高(每个空 key 占用几十字节) |
| 误判风险 | 有假阳性(可接受) | 无误判 |
| 实现复杂度 | 需额外模块/库 | 简单,直接缓存 null 并设置 TTL |
| 适用场景 | 大规模、高频、不存在 key 攻击 | 小规模、低频、临时防护 |
| 支付订单推荐 | 首选方案,拦截 99%+ 穿透 | 可作为补充,对少量误判做兜底 |
最佳实践:在支付订单场景中,建议采用布隆过滤器 + 空值缓存双层防护。布隆过滤器拦截绝大多数不存在请求;对于布隆过滤器误判(假阳性)导致的少量穿透,再通过空值缓存(TTL 60s)兜底,确保数据库零压力。