当前位置: 首页 > news >正文

Redis之缓存穿透

Redis之缓存穿透

在这里插入图片描述

文章目录

  • Redis之缓存穿透
    • 一、什么是缓存穿透?
    • 二、缓存穿透常见的解决方案
      • 1. 缓存空对象(Null Caching)
      • 2. 布隆过滤器(Bloom Filter)​
      • 3. 互斥锁(Mutex Lock)​
      • 4. 接口层校验
      • 5. 热点数据永不过期
      • 6. 缓存预热
      • 7. 实时监控与限流
      • ☆☆☆组合方案推荐☆☆☆
    • 四、实践:缓存空对象

一、什么是缓存穿透?

  • 缓存穿透的定义:缓存穿透(Cache Penetration)是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,导致数据库压力骤增甚至崩溃。
  • 触发原因:恶意攻击、参数伪造、业务逻辑漏洞。
  • 核心问题:大量请求访问数据库中不存在的数据,缓存无法拦截。
  • 示例场景:攻击者发送大量随机ID查询商品信息,这些ID在数据库中不存在,导致每次请求都穿透缓存直达数据库。
请求未命中
请求未命中
客户端
Redis
数据库
  • 与缓存击穿的区别

    • 缓存击穿的定义:缓存击穿(Cache Breakdown)​是指某个热点key​(如爆款商品信息)在缓存中过期后,大量并发请求同时访问数据库(请求数据存在),导致数据库压力骤增。
    • 触发原因:缓存过期时间到期,且高并发场景下请求集中失效。
    • 核心问题:单个热点key失效后,大量请求同时访问数据库。
    • 示例场景:某明星商品突然爆火,缓存中存储的商品信息过期后,所有用户请求同时涌入数据库查询。
    维度缓存穿透缓存击穿
    触发原因请求不存在的数据热点key过期后高并发请求
    数据合法性数据本身不存在(非法参数)数据存在但缓存失效(合法参数)
    攻击性可能是恶意攻击正常业务高并发
    影响范围分散的无效请求集中在某个热点key
    解决方案布隆过滤器、缓存空对象互斥锁、永不过期、后台更新

二、缓存穿透常见的解决方案

1. 缓存空对象(Null Caching)

  • 原理: 当查询数据库发现数据不存在时,将空结果(如 null)写入缓存,并设置较短的过期时间。
  • 优点:简单易实现,直接拦截后续相同请求。
  • 缺点
    • 内存浪费(存储大量无效 null 值)。
    • 可能出现短时不一致,如:数据已补录,但缓存未及时失效。(如需强一致性,可以在更新数据时,删除/覆盖缓存)
  • 实现
    请求未命中
    请求未命中
    缓存null 设置TTL
    客户端
    Redis
    数据库
    public Object getData(String key) {
        // 1. 查询缓存
        Object data = cache.get(key);
        if (data != null) return data;
    
        // 2. 查询数据库
        data = db.query(key);
        if (data == null) {
            // 缓存空对象,设置短期过期时间(如5分钟)
            cache.set(key, "NULL", 5 * 60);
        } else // 正常数据设置较长过期时间
            cache.set(key, data, 60 * 60);
        }
        return data;
    }
    

2. 布隆过滤器(Bloom Filter)​

  • 原理:​在缓存层前加布隆过滤器,预先存储所有合法 Key 的哈希值。查询时先检查布隆过滤器:
    • 若返回“不存在”,直接拦截请求。
    • 若返回“可能存在”,继续查询缓存/数据库。
  • 优点:内存占用低(没有多余的Key),适合海量数据;查询时间复杂度 O(1)。
  • 缺点
    • 存在误的可能(可能将不存在判断为存在)。
    • 实现复杂,删除元素困难(需重建过滤器)。
  • 适用场景:数据量大且允许误判(如黑名单校验)。
  • 实现
    请求
    可能存在,放行
    不存在,拒绝
    命中缓存,返回
    缓存未命中查询数据库
    缓存数据
    客户端
    布隆过滤器
    Redis
    数据库
    // 初始化布隆过滤器(伪代码)
    BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(), expectedInsertions);
    
    // 数据预热时加载存在的key
    db.keys().forEach(k -> bloomFilter.put(k));
    
    public Object getData(String key) {
        // 1. 先查布隆过滤器
        if (!bloomFilter.mightContain(key)) {
            return null; // 直接拦截不存在的key
        }
    
        // 2. 查询缓存/数据库
        Object data = cache.get(key);
        if (data == null) {
            data = db.query(key);
            cache.set(key, data);
        }
        return data;
    }
    

3. 互斥锁(Mutex Lock)​

  • 原理:缓存未命中时,通过互斥锁(如 Redis 的 SETNX)保证只有一个线程查询数据库,其他线程等待回填缓存。
  • 优点:避免大量请求同时穿透到数据库。
  • 缺点
    • 分布式环境下需使用分布式锁(如 Redis RedLock)。
    • 锁竞争可能成为性能瓶颈。
  • 实现
    存在
    不存在
    用户请求数据
    缓存中是否存在?
    返回缓存数据
    尝试获取互斥锁
    获取锁成功?
    查询数据库
    写入缓存
    释放锁
    等待随机时间
    public Object getData(String key) {
        Object data = cache.get(key);
        if (data != null) return data;
    
        // 加锁(如Redis的SETNX)
        String lockKey = "lock:" + key;
        if (redis.setnx(lockKey, "1", 10)) { // 10秒锁超时
            try {
                // 二次检查缓存(防止锁竞争期间其他线程已加载)
                data = cache.get(key);
                if (data != null) return data;
    
                data = db.query(key);
                cache.set(key, data);
            } finally {
                redis.del(lockKey); // 释放锁
            }
        } else {
            // 等待重试
            Thread.sleep(100);
            return getData(key);
        }
        return data;
    }
    

4. 接口层校验

  • 原理:在 API 入口处校验参数合法性,拦截明显无效的请求(如非法 ID 格式、负数等)。
  • 优点:低成本防御恶意攻击(如扫描全表 ID)。
  • 缺点:无法拦截合法参数但实际不存在的数据请求。
  • 示例:校验 ID 是否为正整数、长度是否符合预期。
  • 实现
    public ResponseEntity<?> handleRequest(@PathVariable String id) {
        if (!isValidId(id)) { // 校验ID合法性
            return ResponseEntity.badRequest().build();
        }
        // 继续处理业务逻辑
    }
    

5. 热点数据永不过期

  • 原理:对高频访问的热点数据设置永不过期,通过后台线程主动更新缓存。
  • 优点:彻底避免缓存失效导致的穿透。
  • 缺点:数据一致性依赖更新机制,需处理脏数据问题。
  • 实现:结合定时任务或事件驱动更新缓存。
    // 缓存写入时设置永不过期
    cache.set("hot_key", data);
    
    // 后台定时任务刷新数据
    @Scheduled(fixedRate = 300000)
    void refreshHotData() {
        Object newData = db.query("hot_key");
        cache.set("hot_key", newData);
    }
    

6. 缓存预热

  • 原理:在系统启动或低峰期,预先加载热点数据到缓存中。
  • 优点:减少冷启动时的缓存穿透风险。
  • ​缺点:需提前知道热点数据(可通过历史日志分析)。

7. 实时监控与限流

  • 原理:监控异常流量(如大量 null 响应),触发限流策略(如令牌桶、漏桶算法),保护数据库。
  • 优点:兜底防御,避免突发攻击。
  • 缺点:需配套监控和告警系统。

☆☆☆组合方案推荐☆☆☆

  • 常规场景:缓存空对象 + 接口参数校验。
  • 海量数据:布隆过滤器 + 缓存空对象。
  • 高并发热点数据:永不过期缓存 + 后台更新线程 + 互斥锁。

四、实践:缓存空对象

解决根据id查询商铺信息过程中的缓存穿透

命中
不是
未命中
存在
不存在
开始
提交商铺id
从Redis查询商铺缓存
判断缓存是否命中
判断是否空值
结束
返回商铺信息
根据id查询数据库
判断商铺是否存在
将商铺数据写入Redis
将空值写入Redis
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        // 1.从redis查询商铺缓存
        String key = CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        // 判断命中的是否是空值
        if(shopJson != null) {
            // 返回错误信息,解决缓存穿透问题
            return Result.fail("店铺信息不存在!");
        }

        // 4.不存在,根据id查询数据库
        Shop shop = getById(id);
        if (shop == null) {
            // 5.数据库不存在,将空字串写入Redis,设置过期时间,解决缓存穿透问题
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息,解决缓存穿透问题
            return Result.fail("店铺不存在!");
        }
        // 6.存在,写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

总结:缓存穿透的解决方案需结合业务场景选择,通常需要多种手段协同(如布隆过滤器拦截非法 Key + 缓存空对象减少数据库压力)。同时需权衡内存、一致性和性能,避免过度设计。

相关文章:

  • 【NLP】24. spaCy 教程:自然语言处理核心操作指南(进阶)
  • 《AI大模型应知应会100篇》第5篇:大模型发展简史:从BERT到ChatGPT的演进
  • InnoDB的MVCC实现原理?MVCC如何实现不同事务隔离级别?MVCC优缺点?
  • 基于LangGraph的智能报告生成平台项目分析
  • 树莓派超全系列教程文档--(23)内核参数
  • kubectl命令补全以及oc命令补全
  • ArmSoM Sige5 CM5:RK3576 上 Ultralytics YOLOv11 边缘计算新标杆
  • 【KWDB创作者计划】容器赋能KaiwuDB:探索浪潮数据库KWDB2.2.0 实战指南
  • LLM做逻辑推理题-确定他们的民族
  • JS—大文件上传
  • AI与无人驾驶汽车:如何通过机器学习提升自动驾驶系统的安全性?
  • vs code Cline 编程接入Claude 3.7的经济方案,且保持原生接口能力
  • css 练习01
  • [dp12_回文子串] 最长回文子串 | 分割回文串 IV
  • Kotlin作用域函数
  • MyBatis-Plus笔记(下)
  • 龙虎榜——20250414
  • TLS协议四次握手原理详解,密钥套件采用DH密钥交换算法
  • Video Encoder:多模态大模型如何看懂视频
  • 【HFP】蓝牙 HFP 协议状态通知机制研究
  • 柳向春:关于美国国会图书馆所藏《全芳备祖》的一些故事
  • “模”范生上海,如何再进阶?
  • 降雪致长白山天池景区关闭,有游客在户外等待一小时,景区回应
  • 我给狗狗上课,月赚四五万
  • 国际观察丨澳大利亚新一届政府面临系列挑战
  • 融创中国清盘聆讯延至8月25日,清盘呈请要求遭到部分债权人反对