4. 缓存穿透:定义、产生原因、边界测试场景设计(大量不存在的key)

4.1 定义

缓存穿透 是指客户端请求的数据在缓存中不存在,在数据库中也不存在,导致每次请求都直接穿透缓存层,直达数据库。由于缓存层无法命中,数据库层也查询不到结果,这类请求会绕过缓存保护,对后端数据库造成直接压力。

核心特征: 缓存未命中 + 数据库未命中 = 请求穿透两层存储,直接消耗数据库连接与查询资源。

4.2 产生原因

缓存穿透通常由以下三类场景引发:

原因分类 具体描述 典型示例
恶意攻击 攻击者利用系统业务漏洞,批量请求数据库中不存在的 ID 或 key 遍历不存在的用户 ID:GET /user/9999999
业务误操作 前端或上游服务传入了错误的参数,导致查询了不存在的记录 订单号生成错误,查询已废弃的订单编号
数据未预热 新上线功能或数据迁移后,缓存中无数据,数据库也无对应记录 新商品上架前,客户端提前请求了尚未入库的商品 ID

4.3 缓存穿透流程示意图

下图展示了正常请求与穿透请求的路径差异:

缓存穿透请求路径 vs 正常请求路径 客户端 Redis 缓存 MySQL 数据库 ① 查询缓存 ② 缓存命中 ① 查询缓存(未命中) ② 穿透到数据库 ③ 数据库查询无结果 穿透后果: 大量不存在 key 的请求绕过缓存,直接压向数据库,可能导致数据库连接池耗尽、查询延迟飙升甚至宕机。 正常请求 • 缓存命中 → 直接返回 • 数据库零压力 • 响应时间 < 5ms 穿透请求 • 缓存未命中 → 查数据库 • 数据库空查询消耗连接 • 响应时间 > 50ms

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 测试执行步骤

  1. 准备阶段: 清空 Redis 缓存,确保数据库中存在已知 ID 范围(如 1~100000)。
  2. 基线测试: 使用 1000 个存在的 key 请求,记录正常缓存命中率和数据库 QPS。
  3. 穿透测试: 使用 PT-01 至 PT-06 场景分别执行,每次持续 60 秒。
  4. 监控采集: 通过 Prometheus + Grafana 实时监控 Redis 和 MySQL 指标。
  5. 结果分析: 对比基线数据,评估穿透对系统的影响程度。
  6. 防护验证: 开启布隆过滤器或空值缓存后,重复穿透测试,验证防护效果。
最佳实践: 在测试 PT-02 时,建议使用阶梯式并发(100 → 500 → 1000 → 2000),观察数据库连接池和 CPU 使用率的变化曲线,找到系统的穿透承受阈值。