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 排查步骤

  1. 监控告警:Redis 缓存命中率曲线断崖式下跌,DB 连接数飙升
  2. 日志分析:发现大量请求的 orderId 不在最近 24 小时订单范围内
  3. 流量回放:提取 1000 条异常请求,逐一查询数据库确认均不存在
  4. 确认穿透:缓存中无 key,DB 中无记录,符合穿透定义
实战教训:该案例中,团队最初误判为“缓存击穿”,尝试用互斥锁重建缓存,反而加剧了数据库压力。正确做法是识别出“数据不存在”这一本质,采用布隆过滤器或空值缓存。

4.3 布隆过滤器原理与配置

4.3.1 布隆过滤器核心原理

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素是否可能存在于集合中。它由一个位数组和多个哈希函数组成。

  • 添加元素:将元素通过 k 个哈希函数映射到位数组的 k 个位置,将这些位置置为 1
  • 查询元素:同样计算 k 个哈希位置,如果所有位都是 1,则元素可能存在;如果任意一位为 0,则元素一定不存在
  • 误判率:存在一定的假阳性概率(判断存在但实际不存在),但不会漏判(判断不存在则一定不存在)
支付场景适配:布隆过滤器非常适合拦截“不存在的订单号”。因为订单号一旦生成就固定不变,我们可以将所有合法订单号预加载到过滤器中,查询前先检查,如果过滤器说“不存在”,直接拒绝,不再查询缓存和数据库。
布隆过滤器工作原理(支付订单号校验) 订单号 20250321... Hash 1 Hash 2 Hash 3 位数组(m 位) 1 0 1 0 1 0 0 1 0 1 0 1 判断结果:所有位均为 1 → 可能存在;任意位为 0 → 一定不存在 支付订单号预加载:所有合法订单号 → 布隆过滤器 → 查询前先校验,拦截 100% 的不存在请求

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)兜底,确保数据库零压力。