4. 缓存穿透:定义、产生原因、边界测试场景设计(大量不存在的key)
4.1 定义
缓存穿透 是指客户端请求的数据在缓存中不存在,在数据库中也不存在,导致每次请求都直接穿透缓存层,直达数据库。由于缓存层无法命中,数据库层也查询不到结果,这类请求会绕过缓存保护,对后端数据库造成直接压力。
核心特征: 缓存未命中 + 数据库未命中 = 请求穿透两层存储,直接消耗数据库连接与查询资源。
4.2 产生原因
缓存穿透通常由以下三类场景引发:
| 原因分类 | 具体描述 | 典型示例 |
|---|---|---|
| 恶意攻击 | 攻击者利用系统业务漏洞,批量请求数据库中不存在的 ID 或 key | 遍历不存在的用户 ID:GET /user/9999999 |
| 业务误操作 | 前端或上游服务传入了错误的参数,导致查询了不存在的记录 | 订单号生成错误,查询已废弃的订单编号 |
| 数据未预热 | 新上线功能或数据迁移后,缓存中无数据,数据库也无对应记录 | 新商品上架前,客户端提前请求了尚未入库的商品 ID |
4.3 缓存穿透流程示意图
下图展示了正常请求与穿透请求的路径差异:
4.4 边界测试场景设计
针对缓存穿透,边界测试需要覆盖以下核心场景:
| 测试场景编号 | 场景描述 | 输入数据 | 预期行为 |
|---|---|---|---|
| PT-01 | 单个不存在 key 请求 | GET /api/user?id=999999 |
返回空结果,不写入缓存,数据库仅查询一次 |
| PT-02 | 批量不存在 key 并发请求 | 1000 个并发线程,每个请求不同不存在 ID | 数据库连接数不超过连接池上限,系统不崩溃 |
| PT-03 | 混合存在与不存在 key | 50% 存在 key + 50% 不存在 key | 存在 key 正常返回,不存在 key 不污染缓存 |
| PT-04 | 边界值 key 格式 | 空字符串、null、超长字符串(1024 字符)、特殊字符 | 参数校验拦截,不进入数据库查询 |
| PT-05 | 热点不存在 key 重复请求 | 同一个不存在 key 连续请求 10000 次 | 触发布隆过滤器或空值缓存,数据库查询次数 ≤ 1 |
| PT-06 | 缓存空值过期后穿透 | 空值缓存 TTL 到期后,再次请求同一不存在 key | 允许再次查库,但频率受限流保护 |
4.5 测试验证关键指标
在边界测试执行过程中,需要重点采集以下指标:
- 数据库 QPS 峰值: 穿透场景下数据库每秒查询数,不应超过数据库承载上限的 80%。
- 缓存命中率: 正常请求命中率应 ≥ 95%,穿透攻击时命中率可能骤降至 0%。
- P99 响应时间: 穿透请求的 P99 响应时间不应超过 500ms,否则说明数据库已过载。
- 错误率: 数据库连接超时或拒绝连接的比例应 < 1%。
- 空值缓存写入量: 单位时间内写入 Redis 的空值缓存数量,用于评估缓存空间消耗。
注意: 在测试 PT-02 和 PT-05 时,务必先开启数据库连接池监控和慢查询日志,以便在穿透发生时快速定位数据库压力点。建议在测试环境中模拟真实攻击流量,验证布隆过滤器或空值缓存策略的有效性。
4.6 测试数据构造方法
为了高效构造大量不存在的 key,推荐以下数据生成策略:
// 示例:Java 测试数据生成器
public class PenetrationKeyGenerator {
// 生成 N 个不存在的用户 ID
public static List<String> generateNonExistKeys(int count) {
List<String> keys = new ArrayList<>();
// 已知数据库中最大用户 ID 为 100000
int maxDbId = 100000;
for (int i = 0; i < count; i++) {
// 生成远大于最大 ID 的值,确保数据库不存在
int fakeId = maxDbId + 100000 + i;
keys.add("user:" + fakeId);
}
return keys;
}
// 生成边界值 key 集合
public static List<String> generateBoundaryKeys() {
return Arrays.asList(
"user:",
"user:null",
"user:" + "A".repeat(1024),
"user:__proto__",
"user:<script>alert(1)</script>"
);
}
}
4.7 测试执行步骤
- 准备阶段: 清空 Redis 缓存,确保数据库中存在已知 ID 范围(如 1~100000)。
- 基线测试: 使用 1000 个存在的 key 请求,记录正常缓存命中率和数据库 QPS。
- 穿透测试: 使用 PT-01 至 PT-06 场景分别执行,每次持续 60 秒。
- 监控采集: 通过 Prometheus + Grafana 实时监控 Redis 和 MySQL 指标。
- 结果分析: 对比基线数据,评估穿透对系统的影响程度。
- 防护验证: 开启布隆过滤器或空值缓存后,重复穿透测试,验证防护效果。
最佳实践: 在测试 PT-02 时,建议使用阶梯式并发(100 → 500 → 1000 → 2000),观察数据库连接池和 CPU 使用率的变化曲线,找到系统的穿透承受阈值。