导读:本期聚焦于坚哥创作的《什么是Redis缓存穿透?如何有效预防缓存穿透问题?》,敬请观看详情。查询一个数据库中根本不存在的数据时,请求会绕过Redis直接打到数据库,这就是缓存穿透问题。一旦有恶意用户利用不存在的key发起大量请求,数据库压力会瞬间飙升,严重时甚至导致服务崩溃。本文将深入分析缓存穿透的产生原因和危害,重点讲解几种主流的防护方案,包括缓存空对象、布隆过滤器的原理与实现、接口层参数校验等,并对各方案的适用场景和优缺点进行对比,帮助你根据实际业务选择合适的组合策略,构建更稳健的缓存架构。

在高并发系统中,Redis通常被用作数据库前面的缓冲层,用来抵挡绝大部分读请求。但有一种情况会让缓存完全失效:请求查询的数据在缓存和数据库中都不存在。由于缓存中查不到任何结果,每次请求都会穿透到数据库,这就是所谓的缓存穿透。如果攻击者刻意构造大量不存在的key发起请求,数据库可能在短时间内被拖垮。本文将从问题成因入手,详细讲解几种主流的解决方案及其实现细节。

什么是Redis缓存穿透?如何有效预防缓存穿透问题?

缓存穿透的产生原因与危害

缓存穿透的字面意思是请求像一根针一样穿过缓存层,直接扎在数据库上。正常情况下,一个key第一次被查询时缓存未命中,系统去数据库取数并回填缓存,之后的请求都能命中缓存。但如果这个key在数据库里也不存在,缓存就没有东西可以回填,后续每一个相同key的请求都会继续打到数据库,缓存层形同虚设。

造成这种现象的原因主要有两类:一类是业务层面的正常情况,比如用户访问了一个已经下架的商品、一篇被删除的文章;另一类则是恶意攻击,攻击者用随机生成的id或伪造的用户名发起高频请求,专门探测系统中不存在的数据,这种攻击也常被称为恶意穿透。

危害不容小觑。数据库的连接数和CPU资源是有限的,当穿透请求的量级足够大时,数据库响应变慢,进而拖垮依赖数据库的其他业务,形成连锁故障。因此,在设计缓存架构时,必须提前考虑穿透防护。

方案一:缓存空对象

最直接的做法是:当数据库查询结果为空时,不是什么都不做,而是向Redis写入一个特殊标记值(空对象),并设置一个较短的过期时间。这样同一个key再次被请求时,缓存会命中这个空标记,直接返回空结果,不再访问数据库。

public String queryUser(String userId) {
    String cacheKey = "user:" + userId;
    String value = redis.get(cacheKey);
    // 命中缓存,直接返回(包括空标记)
    if (value != null) {
        return "NULL_MARKER".equals(value) ? null : value;
    }
    // 未命中,查询数据库
    User user = userDao.findById(userId);
    if (user == null) {
        // 数据库中也不存在,写入空标记并设置较短过期时间
        redis.setex(cacheKey, 60, "NULL_MARKER");
        return null;
    }
    String json = JSON.toJSONString(user);
    redis.setex(cacheKey, 3600, json);
    return json;
}

这个方案的优点是实现简单,对业务代码侵入小,不需要引入额外组件。但它有两个明显的缺点:一是会占用大量缓存空间,如果攻击者用海量随机key发起请求,每个key都会在Redis中留下一个空标记,可能把Redis本身打爆;二是存在短暂的数据不一致窗口,如果在空标记有效期内数据库新增了这条数据,用户会查询不到。因此空对象的过期时间不宜设置太长,一般控制在30秒到几分钟之间。

方案二:布隆过滤器

布隆过滤器是一种空间效率极高的概率型数据结构,用来判断某个元素是否存在于集合中。它的特点是:判断不存在就一定不存在,判断存在则有一定概率误判(可能实际不存在)。这个特性非常适合缓存穿透场景,因为在过滤器中被判定不存在的key,可以直接拒绝请求,根本不会触碰缓存和数据库。

底层原理可以简单理解为:一个很长的位数组加上若干个哈希函数。添加元素时,用多个哈希函数对元素计算,把对应的位数组位置置为1;查询元素时,同样计算这若干个位置,只要有一个位置是0,就说明元素肯定不存在,如果全部是1,则认为可能存在。误判率取决于位数组长度和哈希函数个数,可以通过参数调节。

// 基于Redisson实现布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("userFilter");
// 预期插入量为100万,期望误判率为0.01
bloomFilter.tryInit(1000000L, 0.01);

// 系统启动或数据变更时,将所有有效用户id加入过滤器
for (Long userId : userDao.getAllIds()) {
    bloomFilter.add(String.valueOf(userId));
}

// 查询时先经过过滤器拦截
public String queryUser(String userId) {
    // 过滤器判断不存在,直接返回,不会打到数据库
    if (!bloomFilter.contains(userId)) {
        return null;
    }
    String value = redis.get("user:" + userId);
    if (value != null) {
        return value;
    }
    User user = userDao.findById(Long.valueOf(userId));
    if (user == null) {
        redis.setex("user:" + userId, 60, "NULL_MARKER");
        return null;
    }
    String json = JSON.toJSONString(user);
    redis.setex("user:" + userId, 3600, json);
    return json;
}

布隆过滤器的优势是内存占用极小,一亿条数据仅需一百多MB,且查询性能为常数级别。不足之处在于维护成本较高:当数据库新增数据时需要同步往过滤器中添加元素,而删除数据时标准布隆过滤器无法直接删除(可以用计数布隆过滤器解决,但内存开销增大)。因此它更适合数据集合相对稳定或新增操作可控的场景,例如已有的用户id、商品id集合。

方案三:接口层校验与限流兜底

除了缓存层面的防护,还应该在入口处做一层防线。很多穿透请求的参数本身就是非法的,例如id为负数、格式明显错误的手机号、超长字符串等。在接口层对这些参数做合法性校验,可以在请求进入缓存逻辑之前就把它们挡掉。

public Result getUser(String userId) {
    // 参数格式校验,非法参数直接拒绝
    if (userId == null || !userId.matches("\\d{1,10}")) {
        return Result.error("参数非法");
    }
    // 继续后续缓存查询逻辑
    ...
}

对于恶意的穿透攻击,还需要配合限流和熔断机制兜底。可以基于网关或Redis实现接口限流,对单一IP或单一用户的请求频率进行限制;当数据库压力达到阈值时自动熔断降级,返回默认数据或友好提示,避免故障扩散。此外,还可以对同一个key在短时间内的重复查询做合并,也就是常说的请求合并,进一步削减无效流量。

方案对比与选型建议

三种方案各有侧重,实际生产环境通常组合使用。缓存空对象适合处理少量随机的不存在key,实现成本最低;布隆过滤器适合key集合可枚举且相对固定的场景,能以极小的内存代价拦截绝大部分穿透请求;接口校验和限流则是通用的安全兜底手段,无论是否有缓存穿透问题都建议开启。

方案优点缺点适用场景
缓存空对象实现简单,兼容性好占内存,存在短暂不一致不存在key较少且分散
布隆过滤器内存占用小,拦截效率高维护成本高,不支持删除数据集合稳定可预加载
校验与限流防护面广,可对抗攻击无法精确区分业务请求所有场景的兜底防线

推荐的组合策略是:接口层做参数校验和限流作为第一道防线,业务层用布隆过滤器拦截确定不存在的key,再用缓存空对象处理过滤器误判和正常的数据缺失。三者叠加后,基本可以杜绝缓存穿透对数据库的冲击,让整个缓存体系更加稳固可靠。

Redis缓存穿透布隆过滤器缓存空对象修改时间:2026-09-01 10:58:55

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。