什么是缓存穿透?

缓存穿透是指攻击者或异常请求频繁查询缓存和后端数据库中都不存在的数据,导致每次请求都直接穿透缓存,命中数据库,增加数据库压力,可能导致系统性能下降甚至崩溃。

产生原因

  1. 恶意攻击:攻击者故意请求不存在的 key(如随机生成的 ID)。
  2. 业务逻辑问题:用户查询的数据在系统中不存在(如查询无效商品 ID)。
  3. 缓存失效:热点数据未及时缓存,导致请求直接访问数据库。

影响

  • 数据库负载激增,响应变慢。
  • 系统吞吐量下降,可能引发雪崩效应。

如何解决缓存穿透?

以下是常见的解决方案,结合实际场景选择使用:

  1. 缓存空对象

    • 方法:当查询的数据在数据库中不存在时,在缓存中存储一个空对象(如 null 或空字符串),并设置较短的过期时间。
    • 优点:简单有效,减少对数据库的直接访问。
    • 缺点:可能占用缓存空间,需设置合理过期时间。
    • 示例(伪代码):
      1
      2
      3
      4
      5
      6
      7
      8
      value = cache.get(key)
      if value is None:
      value = db.query(key)
      if value is None:
      cache.set(key, "", expire=60) # 缓存空对象,过期时间 60 秒
      else:
      cache.set(key, value, expire=3600)
      return value
  2. 布隆过滤器(Bloom Filter)

    • 方法:在缓存前部署布隆过滤器,预先将数据库中存在的 key 映射到布隆过滤器中。查询时,先检查 key 是否在布隆过滤器中,若不在则直接返回,阻止穿透。
    • 优点:内存占用小,查询效率高,适合大规模数据场景。
    • 缺点:存在误判率(可能误认为存在的 key 不存在),需定期更新布隆过滤器。
    • 适用场景:数据量大,存在性查询频繁。
    • 示例:使用 Redis 的布隆过滤器插件(如 redis-bloom)。

    贴一个布隆过滤器

    布隆过滤器(Bloom Filter)是一种空间高效的概率性数据结构,用于快速判断一个元素是否可能存在于一个集合中。它以极小的内存占用和高效的查询性能著称,广泛应用于缓存穿透、大数据去重、分布式系统等场景。

    核心原理

    布隆过滤器基于以下机制:

    1. 数据结构:一个长度为 mm 的位数组(bit array),初始全为 0。
    2. 哈希函数:使用 kk 个独立的哈希函数,将元素映射到位数组的 kk 个位置。
    3. 添加元素:对元素计算 kk 个哈希值,将对应的位数组位置置为 1。
    4. 查询元素
      • 计算元素的 kk 个哈希值,检查对应位是否全为 1。
      • 若全为 1,元素可能存在;若任一位为 0,元素一定不存在

    特点

    • 优点
      • 空间效率高:只需存储位数组,远小于实际数据。
      • 查询速度快:时间复杂度为 O(k)O(k),与集合大小无关。
    • 缺点
      • 误判率:可能将不存在的元素误判为存在(false positive),但不会漏判(false negative)。
      • 不可删除:传统布隆过滤器不支持元素删除(因为多个元素可能共享同一位置)。
  3. 参数校验与限流

    • 方法
      • 在接口层对请求参数进行校验,拦截明显无效的请求(如非数字 ID、超范围值)。
      • 使用限流机制(如 Nginx 或 Redis)限制同一 IP 或用户的请求频率。
    • 优点:从源头减少无效请求,保护后端。
    • 缺点:可能误伤合法请求,需精细配置。
    • 示例(Nginx 限流):
      1
      2
      3
      4
      5
      6
      limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
      server {
      location /api {
      limit_req zone=mylimit burst=20;
      }
      }
  4. 异步缓存预热

    • 方法:在系统启动或数据变更时,提前将热点数据加载到缓存中,减少查询不存在数据的可能。
    • 优点:从源头降低穿透风险。
    • 缺点:需额外维护缓存预热逻辑。
    • 示例:定时任务将数据库热点数据写入 Redis。
  5. 设置默认值

    • 方法:对于某些业务场景,查询不存在的数据时返回默认值(如空列表),并缓存默认值。
    • 优点:对用户友好,减少数据库压力。
    • 缺点:需根据业务场景设计默认值。
    • 示例:查询用户订单列表,若无订单,缓存空列表 []
  6. 数据库降级或熔断

    • 方法:当检测到大量穿透请求时,触发降级机制(如直接返回错误)或熔断,暂停对数据库的访问。
    • 优点:保护数据库,防止系统崩溃。
    • 缺点:可能影响正常用户体验。
    • 示例:使用 Hystrix 或 Sentinel 实现熔断。

总结

  • 缓存穿透:查询不存在数据导致请求穿透缓存,命中数据库。
  • 解决方案
    1. 缓存空对象(简单快速)。
    2. 布隆过滤器(高效,适合大数据)。
    3. 参数校验与限流(源头拦截)。
    4. 缓存预热(预防为主)。
    5. 设置默认值(用户友好)。
    6. 降级或熔断(保护系统)。

推荐实践

  • 小型系统:优先使用缓存空对象 + 参数校验。
  • 大型系统:结合布隆过滤器 + 限流 + 缓存预热。
  • 监控:监控缓存命中率和数据库 QPS,及时发现穿透问题。