🌪️ Redis缓存穿透:当数据库被“空气”攻击时,如何优雅防御?

🌪️ Redis缓存穿透:当数据库被"空气"攻击时,如何优雅防御?

"缓存穿透?就是Redis防了个寂寞,数据库被空气打了!"

大家好!今天我们来聊聊Redis缓存中那个让无数开发者头疼的"幽灵攻击"------缓存穿透。它不费一兵一卒,却能让你数据库原地爆炸。本文将从原理到实战,手把手教你如何优雅防御!


一、缓存穿透:一场针对"不存在数据"的恶意攻击

什么是缓存穿透?

当用户疯狂请求数据库中根本不存在的数据 时,请求会绕过缓存层(因为缓存中也没有),直接穿透到数据库。高并发下,数据库可能被压垮。

举个例子🌰:

  • 正常请求:查商品ID=1001(存在) → 缓存命中 → 返回结果
  • 穿透请求:查商品ID=-1(不存在) → 缓存未命中 → 查数据库 → 数据库返回空 → 下次继续查库(死循环)

二、防御方案对比:哪种才是你的菜?

方案 原理 优点 缺点
空对象缓存 将"不存在"也缓存 简单直接,成本低 可能缓存大量无用数据
布隆过滤器 预存所有合法Key,拦截非法请求 内存占用极小,效率高 存在误判率,不能删除元素
互斥锁 防止大量请求同时击穿到DB 保证数据一致性 降低系统并发能力

三、实战代码:Java防御三件套

场景:电商平台商品查询

java 复制代码
// 1. 布隆过滤器初始化(Guava实现)
BloomFilter<Long> bloomFilter = BloomFilter.create(
    Funnels.longFunnel(), 
    1000000, // 预期元素数量
    0.01     // 误判率
);

// 初始化合法商品ID(实际应从DB加载)
bloomFilter.put(1001L);
bloomFilter.put(1002L);

// 2. 商品查询服务(三层防护)
public Product getProduct(Long id) {
    // 第一关:布隆过滤器拦截
    if (!bloomFilter.mightContain(id)) {
        log.warn("🚫 非法ID被拦截: {}", id);
        return null;
    }
    
    // 第二关:尝试从缓存读取
    String cacheKey = "product:" + id;
    Product product = redisTemplate.opsForValue().get(cacheKey);
    
    // 命中空对象缓存(特殊标记)
    if ("NULL_OBJECT".equals(product)) {
        log.info("🛑 命中空对象缓存: {}", id);
        return null;
    }
    
    // 第三关:缓存未命中,查数据库(加互斥锁)
    if (product == null) {
        synchronized (this) {
            // 双重检查锁
            product = redisTemplate.opsForValue().get(cacheKey);
            if (product == null) {
                // 查数据库
                product = productDao.findById(id);
                if (product == null) {
                    // 缓存空对象,设置短TTL
                    redisTemplate.opsForValue().set(cacheKey, "NULL_OBJECT", 5, TimeUnit.MINUTES);
                } else {
                    // 缓存真实数据
                    redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
                }
            }
        }
    }
    return product;
}

关键点解析:

  1. 布隆过滤器:前置屏障,拦截绝对非法ID(如负数、超大数)
  2. 空对象缓存:为不存在的Key设置特殊值(避免重复查库)
  3. 互斥锁:synchronized保证单进程重建缓存(分布式用Redisson)

四、原理深潜:布隆过滤器为何如此高效?

布隆过滤器工作流程:

markdown 复制代码
         ┌─────────┐       ┌─────────┐
请求ID →  │ 哈希函数1 │ → 位位置1 │         │
         ├─────────┤       │         │
         │ 哈希函数2 │ → 位位置2 │ 位数组   │ → 全为1?存在!
         ├─────────┤       │         │
         │ 哈希函数3 │ → 位位置3 │         │
         └─────────┘       └─────────┘
  • 写入:对Key做K次哈希,将位数组对应位置设为1
  • 查询:检查Key的所有哈希位是否均为1(是→可能存在,否→绝对不存在)
  • 特点宁可错杀一千,绝不放过一个(可能误判合法Key,但绝不放过非法Key)

五、避坑指南:这些雷区千万别踩!

  1. 空值缓存TTL过短

    → 攻击者会在TTL过期后再次穿透

    ✅ 解决方案:动态TTL,例如30s + 随机30s

  2. 布隆过滤器初始化不全

    → 新商品ID被误判为非法

    ✅ 解决方案:启动时全量加载+定时增量同步

  3. 过度依赖单一方案

    → 布隆过滤器有误判率,空对象浪费内存

    ✅ 终极方案:组合拳!布隆过滤器+空缓存+互斥锁


六、最佳实践:企业级防御架构

graph LR A[客户端请求] --> B{布隆过滤器} B -->|合法| C[Redis缓存] B -->|非法| D[直接拒绝] C -->|命中| E[返回数据] C -->|未命中| F{获取分布式锁} F -->|成功| G[查询数据库] G -->|存在| H[写入缓存] G -->|不存在| I[写入空缓存] F -->|失败| J[短暂等待后重试缓存]

七、面试暴击点:这样答秒杀面试官!

问题1:缓存穿透和缓存击穿有什么区别?

💡 答:

  • 穿透:查不存在的数据(恶意攻击)
  • 击穿:查存在但过期的热点Key(并发访问导致)

问题2:布隆过滤器为何不能删除元素?

💡 答:

删除元素需将对应位置0,但该位可能被其他元素共享(哈希碰撞)。误删会导致其他元素被误判为不存在!

问题3:如何动态更新布隆过滤器?

💡 答:

  • 方案1:重建新布隆过滤器 → 原子替换旧版本
  • 方案2:使用Counting Bloom Filter(支持计数删除)

八、总结:防御之道在于分层拦截

"不要让你的数据库为空气打工!"

缓存穿透的本质是对不存在数据的暴力访问,防御核心思路:

  1. 前置拦截:布隆过滤器阻挡明显非法请求(门卫大爷)
  2. 中端缓存:空对象缓存吸收重复攻击(僵尸尸体也有价值)
  3. 后端加锁:互斥锁防止并发击穿(排队别挤!)

终极奥义 :没有银弹!组合方案 + 动态调整才是王道。


思考题:如果攻击者使用随机生成的不存在ID(如UUID),布隆过滤器还有效吗?欢迎评论区讨论! 👇

相关推荐
Hellyc2 小时前
用户查询优惠券之缓存击穿
java·redis·缓存
鼠鼠我捏,要死了捏4 小时前
缓存穿透与击穿多方案对比与实践指南
redis·缓存·实践指南
天河归来8 小时前
springboot框架redis开启管道批量写入数据
java·spring boot·redis
守城小轩8 小时前
Chromium 136 编译指南 - Android 篇:开发工具安装(三)
android·数据库·redis
Charlie__ZS9 小时前
若依框架去掉Redis
java·redis·mybatis
汤姆大聪明11 小时前
Redis 持久化机制
数据库·redis·缓存
钩子波比11 小时前
🚀 Asynq 学习文档
redis·消息队列·go
也许明天y13 小时前
Spring Cloud Gateway 自定义分布式限流
redis·后端·spring cloud
kk在加油13 小时前
Redis数据安全性分析
数据库·redis·缓存
都叫我大帅哥16 小时前
🔥 Redis缓存击穿:从“崩溃现场”到“高并发防弹衣”的终极指南
redis