2. 缓存基础回顾:缓存穿透、缓存击穿、缓存雪崩的定义与常见解决方案
在构建高并发、低延迟的数据库缓存系统时,缓存穿透、缓存击穿与缓存雪崩是三个最典型的异常场景。它们不仅会导致数据库负载飙升,还会直接破坏缓存 API 的响应格式契约——例如本该返回 200 OK + JSON 的接口,可能因上游崩溃而返回 502 或超时。本节从定义、成因与解决方案三个维度进行系统回顾。
2.1 三种异常的定义与对比
| 异常类型 | 核心定义 | 影响范围 | 典型触发条件 |
|---|---|---|---|
| 缓存穿透 | 请求查询一个一定不存在的数据,缓存与数据库均无命中,每次请求都穿透到数据库。 | 单点数据,但可被恶意利用放大 | 查询不存在的 ID、爬虫攻击、参数校验缺失 |
| 缓存击穿 | 某个热点 key 在缓存失效的瞬间,大量并发请求同时回源数据库。 | 单个热点 key | 热点数据过期、缓存淘汰策略触发 |
| 缓存雪崩 | 大量 key 在同一时间段集中失效,或缓存节点宕机,导致请求全部落到数据库。 | 全局或大面积缓存 | 统一过期时间、缓存集群故障、重启后缓存清空 |
契约测试视角: 三种异常都会导致缓存 API 返回非预期的状态码或响应体。例如穿透时可能返回
null 而非标准错误 JSON;雪崩时可能直接返回 503 Service Unavailable。契约测试必须覆盖这些边界响应格式。
2.2 缓存穿透:定义与解决方案
定义: 客户端请求一个缓存和数据库中都不存在的数据(如 GET /user?id=-1),由于缓存未命中,每次请求都穿透到数据库查询,数据库也返回空结果,导致缓存始终无法建立,数据库压力剧增。
常见解决方案
- 参数校验与布隆过滤器:在 API 入口层校验请求参数合法性,对不可能存在的 ID 直接拒绝;使用布隆过滤器(Bloom Filter)预判 key 是否存在,不存在则直接返回。
- 缓存空对象:即使数据库查询结果为空,也缓存一个短生命周期的空值(如
null或{}),避免重复穿透。 - 接口限流与降级:对同一 IP 或同一参数的频繁穿透请求进行限流,触发降级返回默认响应。
契约测试关注点: 缓存空对象的响应格式必须与正常数据一致(例如统一返回
{"code":0,"data":null}),不能直接返回 404 或空 body,否则客户端解析会失败。
2.3 缓存击穿:定义与解决方案
定义: 一个被高频访问的热点 key 在缓存过期的瞬间,大量并发请求同时发现缓存失效,全部回源数据库查询,导致数据库瞬时压力激增,甚至引发连接池耗尽。
常见解决方案
- 互斥锁(Mutex Lock):在缓存失效时,只允许一个线程去数据库加载数据,其他线程等待或重试。常用分布式锁如 Redis SETNX。
- 逻辑过期 + 异步刷新:缓存永不过期,但存储一个逻辑过期时间;后台线程检测到逻辑过期后异步更新缓存,请求仍返回旧数据。
- 热点 key 永不过期:对已知热点数据设置物理永不过期,通过定时任务或消息队列主动更新。
注意: 互斥锁方案会增加响应延迟,且可能引入死锁风险。契约测试需要验证锁超时或获取锁失败时的降级响应格式是否符合契约。
2.4 缓存雪崩:定义与解决方案
定义: 大量缓存 key 在同一时间失效(如统一设置 1 小时过期),或缓存节点发生宕机/重启,导致所有请求直接打到数据库,造成数据库负载瞬间飙升,甚至引发级联崩溃。
常见解决方案
| 策略 | 实现方式 | 适用场景 |
|---|---|---|
| 过期时间随机化 | 在基础过期时间上增加随机偏移(如 ±30%),避免集中失效 | 所有可设置 TTL 的缓存 |
| 缓存高可用 | Redis 主从 + Sentinel / Cluster,避免单点故障 | 生产环境核心缓存 |
| 多级缓存 | 本地缓存(Caffeine/Guava) + 分布式缓存(Redis)组合 | 读多写少、允许最终一致 |
| 限流熔断 | 对数据库访问进行限流,超出阈值直接熔断返回降级数据 | 数据库保护 |
2.5 三种异常对缓存 API 响应格式的影响
从契约测试的角度,三种异常不仅影响系统可用性,更直接破坏缓存 API 的响应格式契约。下表总结了典型影响:
| 异常类型 | 预期正常响应 | 异常时可能出现的响应 | 契约测试校验点 |
|---|---|---|---|
| 缓存穿透 | {"code":0,"data":{...}} |
null、404、空 body |
空对象响应格式是否统一 |
| 缓存击穿 | 200 OK + JSON |
503、超时、{"code":-1} |
降级响应是否包含标准错误码 |
| 缓存雪崩 | 200 OK + JSON |
502 Bad Gateway、连接拒绝 |
熔断时是否返回固定 fallback 格式 |