找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4095

积分

0

好友

529

主题
发表于 4 小时前 | 查看: 4| 回复: 0

很多系统都会在数据库前面加一层 Redis 做缓存。

请求来了,先查缓存;缓存里有就直接返回,没有再查数据库,并把查询结果写回缓存。

这个流程平时运行得很好,但有一天,数据库突然扛不住了。监控显示,每秒有上万次查询直接落到了数据库上,而 Redis 的命中率几乎为零。

查日志后发现,请求里的订单 ID 根本不存在。

缓存里没有这些订单,数据库里也查不到。可每次请求到来,系统还是会老老实实地先查缓存、再查数据库。大量请求就这样轻松绕过了缓存,全部压到数据库上。

这就是缓存穿透。

缓存穿透安全科普插画:黑客用不存在的ID攻击系统,导致Redis未命中,请求穿透直达崩溃的数据库

先用一个例子说清楚

可以把缓存想象成学校收发室的取件架,把数据库想象成后面的仓库。

  • 查询一个存在的订单,就像取一件真实存在的快递。取件架上有,直接拿走;架子上没有,才去仓库找。
  • 查询一个不存在的订单,就像拿着一个错误的取件码来取快递。取件架上没有,仓库里也没有,但工作人员仍然要跑一趟仓库去确认。

偶尔查错一次没什么影响。可如果有人每秒提交上万个完全错误的取件码,仓库再大也经不起这样反复折腾。

所以,缓存穿透指的是:请求的数据既不在缓存中,也不在数据库中,导致请求一次次越过缓存,直接访问数据库。

正常查询和缓存穿透有什么不同?

正常情况下:

请求 → 查缓存 → 命中 → 返回
         ↓ 未命中
       查数据库 → 查到数据 → 写入缓存 → 返回

发生缓存穿透时:

请求 → 查缓存 → 未命中
         ↓
       查数据库 → 仍然没有
         ↓
       返回空结果
         ↓
下一次查询相同 ID 时,再走一遍数据库

问题不在于某一次查询,而在于这些不存在的数据无法被普通缓存记住。同一个无效 ID 被反复查询,或者大量随机无效 ID 同时出现时,数据库的压力就会持续居高不下。

你可能会问,那该怎么应对?下面介绍三种常见的处理办法。

方案一:缓存空值

最直接的办法,是把“数据库里没有这条数据”这个事实也记录到缓存中。

下次再查询同一个 ID,系统看到我们设置的空值标记,就可以直接返回,不必再次访问数据库。

def get_order(order_id):
    # 1. 先查缓存
    data = redis.get(f"order:{order_id}")
    if data is not None:
        if data == "NULL":  # 空值标记
            return None
        return data

    # 2. 缓存未命中,再查数据库
    order = db.query("SELECT * FROM orders WHERE id = ?", order_id)

    if order is None:
        # 3. 数据库也没有,短时间缓存空值
        redis.setex(f"order:{order_id}", 60, "NULL")
        return None

    # 4. 查到数据,正常写入缓存
    redis.setex(f"order:{order_id}", 3600, order)
    return order

空值通常会设置一个较短的过期时间,比如 60 秒。这样一来可以避免长时间占用缓存空间,二来也能避免数据刚写入数据库后,接口仍然一直返回空结果。

优点 局限
实现简单,改动小 每个不存在的 ID 也会占用一个缓存 Key
能挡住同一个无效 ID 的重复查询 如果无效 ID 不断变化,仍可能占用大量内存
容易接入现有缓存流程 过期时间需要根据业务情况调整

这种方案适合处理少量、重复出现的无效 ID。

方案二:布隆过滤器

如果无效 ID 是随机生成的,缓存空值就不太划算了。攻击者每次换一个 ID,Redis 里就会不断增加新的空值 Key。

这时,可以在缓存前面加一层布隆过滤器(Bloom Filter)

布隆过滤器是一种非常节省内存的数据结构,适合用来判断一个元素是否可能存在。它的判断结果只有两种:

  • 肯定不存在:可以直接拦截,不再查询缓存和数据库。
  • 可能存在:继续走正常查询流程。

注意,“可能存在”不等于“一定存在”。布隆过滤器有一定的误判概率,它可能把一个本不存在的 ID 判断为“可能存在”,但它绝不会把已经加入过滤器的 ID 漏判为“不存在”。

它是怎么工作的?

布隆过滤器底层可以看成一个很长的二进制数组,开始时所有位置都是 0。

初始状态:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

添加 ID=10086:
用 3 个哈希函数算出位置 3、7、12,并把这些位置设为 1
[0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0]

以后再查询某个 ID 时,用同样的哈希函数计算位置:

  • 如果其中任意一位是 0,这个 ID 肯定没有加入过过滤器。
  • 如果所有位置都是 1,这个 ID 可能加入过,也可能只是发生了哈希碰撞。

在缓存查询中,可以这样使用:

# 系统初始化时,把已有的合法 ID 加入布隆过滤器
bloom_filter = BloomFilter(capacity=1_000_000, error_rate=0.01)
for order_id in db.query("SELECT id FROM orders"):
    bloom_filter.add(order_id)

def get_order(order_id):
    # 1. 先经过布隆过滤器
    if not bloom_filter.contains(order_id):
        return None  # 肯定不存在,不查缓存,也不查数据库

    # 2. 可能存在,再走正常的缓存和数据库查询流程
    data = redis.get(f"order:{order_id}")
    if data is not None:
        return data

    order = db.query(...)
    # 后续按正常流程处理
优点 局限
很省内存,适合保存大量 ID 存在一定误判概率
查询速度快 新增数据时要同步更新过滤器
能提前拦住大量不存在的 ID 普通布隆过滤器不支持直接删除元素

布隆过滤器更适合数据量大、无效 ID 随机变化的场景。

方案三:接口参数校验

还有一道很基础、但经常被忽略的防线:先判断参数是否合法。

def get_order(order_id):
    # 第一道防线:检查参数格式和取值范围
    if not isinstance(order_id, int) or order_id <= 0 or order_id > 10_000_000:
        return None

    # 参数合法,再走缓存和数据库查询流程
    ...

负数、超大数字、特殊字符这类明显不合法的参数,可以直接拒绝,连 Redis 都不必查询。

不过,参数校验只能挡住“格式或范围不合法”的请求。如果攻击者使用格式正确、但实际不存在的 ID,仍然需要缓存空值或布隆过滤器来兜底。

三种方案怎么选?

场景 更合适的方案
少数几个无效 ID 被反复查询 缓存空值
无效 ID 随机变化,而且请求量大 布隆过滤器
合法 ID 数量多,需要节省内存 布隆过滤器
不能接受过滤器误判 缓存空值
数据频繁新增 缓存空值,或使用支持删除和计数的布隆过滤器变体
参数有明确的格式和范围 接口参数校验

在实际项目里,这三种方法通常不是三选一,而是搭配使用:

请求 → 参数校验 → 布隆过滤器 → 查缓存 → 查数据库
                                      ↓          ↓
                                   命中返回   数据不存在时缓存空值

参数校验先拦住明显不合法的请求;布隆过滤器过滤掉绝大部分不存在的 ID;缓存空值再处理少量漏网的重复查询。

小结

缓存穿透的原理并不复杂:查询的数据根本不存在,缓存没能挡住请求,数据库就只能被反复访问。

处理时可以记住三点:

  • 重复查询少量无效 ID,可以缓存空值。
  • 无效 ID 数量大、变化快,可以使用布隆过滤器。
  • 参数有明确规则,一定要先做接口校验。

单独使用一种方案未必能覆盖所有情况。根据数据规模和请求特点组合使用,才能真正减轻数据库压力。

对于这类高并发场景下的系统设计问题,通常没有一招鲜的银弹,更多是依据业务特点做出权衡与组合。如果你对后端架构中这类问题的处理思路感兴趣,可以到【云栈社区】看看更多关于高可用的实战讨论。


用简单的话,讲清楚一个技术问题。我们明天见!




上一篇:互联网大厂「滚动式裁员」渐成常态:630之后,再也没有安全期
下一篇:Kimi K3 公文写作实测:对比 GPT-5.6 与 Claude Opus 4.8 效果如何?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-7-19 08:46 , Processed in 0.779571 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表