什么是缓存穿透?
缓存穿透(Cache Penetration)是缓存系统中常见的一种性能问题,指大量请求查询数据库中不存在的数据,导致请求直接绕过缓存层持续冲击数据库,从而引发数据库压力过大甚至崩溃的情况。
它与 缓存击穿(Cache Breakdown) 和 缓存雪崩(Cache Avalanche) 的区别:
- 缓存穿透: 查询的是数据库中根本不存在的数据。
- 缓存击穿(详细介绍): 查询的是数据库中存在的数据,但在缓存中恰好过期,此时有大量并发请求涌入数据库。
- 缓存雪崩(详细介绍): 缓存中的大批量数据同时过期,导致大量请求直接访问数据库。
缓存穿透是如何发生的?
- 恶意攻击/爬虫: 攻击者故意构造大量数据库里根本不存在的键(Key)来发起请求(例如,请求不存在的用户ID、商品ID、订单号等)。
- 业务逻辑缺陷: 应用本身逻辑问题,错误地生成了无效的查询键(如输入校验不严,用户输入了无效ID)。
- 数据自然淘汰: 某些曾经存在但已被删除的数据,其对应的键可能还在被请求(这种情况相对少见,主要问题还是前两点)。
缓存穿透的危害
- 数据库压力剧增: 缓存本应作为数据库的 “盾牌”,穿透会让盾牌失效,数据库直接暴露在高并发请求下。
- 数据库性能瓶颈: 数据库处理能力有限,面对大量无效查询,其CPU、内存、IO资源会被迅速耗尽。
- 服务响应延迟或不可用: 极端情况下,数据库可能因连接耗尽、CPU 占用过高而崩溃,进而导致依赖它的服务瘫痪,最终表现为整个应用卡顿或宕机。
- 资源浪费: 缓存层完全失效,失去了其存在的意义。
如何解决缓存穿透?
解决的核心思路是:即使数据库中没有数据,也要在缓存层形成一种“存在”的标记或结果,阻止请求穿透到数据库。 常用且有效的解决方案包括:
1. 缓存空对象(Null Caching / Cache Empty Result)
- 原理: 当查询数据库返回空结果(即数据不存在)时,将这个“空结果”也缓存起来(例如:用户查询ID=12345的用户,数据库无结果 → 缓存key=user:12345,value=null,过期时间 3 分钟 → 3 分钟内的相同请求直接返回null)。
- 过程:
- 请求查询 Key X。
- 缓存未命中。
- 查询数据库,发现数据不存在。
- 将 Key X 与一个表示“空”的值(并设置一个相对较短的过期时间,如 3-30分钟)写入缓存。
- 后续请求查询 Key X 时,直接从缓存中获取到“空”值返回,不再访问数据库。
- 优点: 实现简单,能有效拦截短时间内对同一个不存在Key的重复请求。
- 缺点:
- 内存浪费: 如果攻击者构造大量不同的、随机的不存在Key,缓存中会存储大量无意义的空对象,占用宝贵的内存空间。
- 数据短期不一致: 如果在空对象缓存有效期内,数据库实际添加了这个Key对应的数据,那么应用在空对象过期前会一直返回空/错误结果。可以通过设置较短的过期时间或主动删除缓存来缓解。
- 适用场景: 适合攻击Key相对有限或重复率较高的情况。通常与其他方案(如布隆过滤器)结合使用。
2. 布隆过滤器(Bloom Filter)
-
原理: 布隆过滤器是一种空间效率极高的概率型数据结构。它使用一个很长的二进制向量(Bit Array)和一系列随机映射函数(Hash函数)来存储和检测某个元素是否可能存在于一个集合中。
- 添加元素: 将元素通过 K 个不同的哈希函数映射到位数组的 K 个位置,并将这些位置置为 1。
- 检查元素:
- 如果元素通过 K 个哈希函数映射到的 K 个位置都是 1,则认为元素“可能存在”(存在一定的误判率)。
- 如果任何一个位置是 0,则肯定不存在。
-
应用解决缓存穿透:
- 将所有可能存在的、有效的查询Key(例如,所有有效的用户ID、商品ID)预先加载到布隆过滤器中。
- 当请求到来时:
- 先用布隆过滤器检查请求的 Key。
- 如果布隆过滤器说 Key 不存在(肯定不存在) -> 直接返回空结果或错误响应给客户端,不再查询缓存或数据库。(这是拦截穿透的关键!)
- 如果布隆过滤器说 Key 可能存在 -> 继续走正常的缓存查询流程(查缓存 -> 缓存命中则返回;缓存未命中则查数据库 -> 数据库查到则回填缓存;数据库查不到则缓存空对象)。
-
优点:
- 空间效率极高: 相比存储完整Key或空对象,布隆过滤器占用空间极小(存储 1 亿个 ID 仅需约 100MB)。
- 查询效率极高: 检查一个Key是否存在的时间复杂度是 O(K)(K是哈希函数个数)。
- 有效拦截: 能准确拦截数据库中肯定不存在的Key的请求。
-
缺点:
- 误判率(False Positive): 可能存在误判(即布隆过滤器认为Key存在,但实际上数据库不存在)。这是概率性的,但可以通过增加位数组大小和哈希函数数量来降低误判率(代价是空间和计算时间增加)。
- 删除困难: 标准的布隆过滤器不支持元素删除(因为多个元素可能共享同一个bit位)。可以使用变种如计数布隆过滤器(Counting Bloom Filter)支持删除,但空间开销更大。
- 初始化/维护: 需要预加载有效Key集合,并在数据库新增/删除有效Key时同步更新布隆过滤器(有一定延迟或复杂性)。
-
适用场景: 非常适合解决大规模、随机Key的恶意攻击造成的穿透问题。是当前最主流和推荐的解决方案。
3. 接口层校验(Validation)
- 原理: 在请求到达缓存 / 数据库前,通过接口层(如网关、Controller)过滤明显无效的参数,直接拦截恶意请求。
- 方法:
- 格式校验: 检查ID是否符合规则(如必须是正整数、长度范围、符合特定格式如UUID)。
- 范围校验: 检查ID是否在已知的有效范围内(如果业务允许定义范围)。
- 业务规则校验: 根据业务逻辑判断请求是否合理(例如,查询一个状态为“已删除”的订单)。
- 优点: 简单直接,能过滤掉一部分明显无效的请求。
- 缺点: 对于构造精良、看起来符合规则的无效Key(如随机生成但在有效范围内的ID)无法拦截。无法防御大规模、随机的无效Key攻击。
- 适用场景: 作为基础防护,必须与其他方案(布隆过滤器、空对象缓存)结合使用。是防御的第一道门槛。
4. 实时监控与风控
- 原理: 对系统的访问模式进行实时监控,识别异常流量(如大量请求命中空对象缓存、大量请求直接访问数据库且返回404)。
- 方法:
- 设置告警阈值(如单位时间内访问不存在Key的频率)。
- 对识别出的恶意IP或用户行为模式进行限流(Rate Limiting)、封禁(IP Blocking)或人机验证(CAPTCHA)。
- 优点: 能主动发现并应对攻击行为,保护系统。
- 缺点: 属于事后补救或动态防御,需要一定的监控和风控系统建设成本。不能完全替代前几种在数据访问层的防御措施。
- 适用场景: 作为补充防护手段,用于发现和缓解正在进行的攻击。
总结与最佳实践建议
- 布隆过滤器是核心: 对于防御大规模、随机的无效Key攻击,布隆过滤器是最有效、空间效率最高的方案。务必在缓存层之前部署。
- 空对象缓存作为补充: 对于通过布隆过滤器“可能存在”检查但最终数据库查不到的请求(包括误判和少量新攻击Key),使用缓存空对象进行短期拦截,避免重复穿透。
- 严格参数校验是基础: 在应用入口处进行严格的格式、范围和业务逻辑校验,过滤掉大部分明显无效的请求,减轻后续压力。
- 监控风控不可少: 建立实时监控和风控机制,及时发现异常流量模式并采取限流、封禁等措施。
- 空对象缓存过期时间: 为缓存空对象设置一个合理的、较短的过期时间(几分钟到几十分钟),平衡内存占用和数据不一致的时间窗口。
- 布隆过滤器维护: 确保布隆过滤器能跟随数据库有效Key集合的变化(新增、删除)进行同步更新(可能需要异步任务或监听binlog)。考虑使用支持删除的变种(如Counting Bloom Filter)或定期重建。
- 组合使用: 在实际生产环境中,通常需要组合使用多种方案(如
校验 -> 布隆过滤器 -> 缓存 -> 数据库
,并在缓存未命中且数据库查不到时写入空对象),形成多层次的防御体系。
通过综合运用这些策略,可以有效地解决缓存穿透问题,保护数据库免受无效请求的冲击,确保系统的稳定性和高性能。