🌪️ 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),布隆过滤器还有效吗?欢迎评论区讨论! 👇

相关推荐
IT大白鼠2 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz2 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙2 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除2 天前
redis--018
redis
两锭子2 天前
Redis基础
redis
烟沙九洲2 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索2 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞2 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路3 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog3 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis