Redis 缓存击穿、缓存雪崩和缓存穿透

在缓存系统(如Redis、Memcached)的使用中,缓存击穿缓存雪崩是两种常见的性能问题,均会导致大量请求直接穿透到数据库,引发数据库压力骤增甚至宕机。但二者的场景和原因不同,解决方案也各有侧重。

一、缓存击穿(Cache Breakdown)

定义

缓存击穿是指:某个“热点Key”(被高频访问的Key)在缓存中失效(过期或不存在)的瞬间,大量并发请求直接穿透到数据库,导致数据库瞬间压力过大

例如:秒杀活动中的商品ID(热点Key)缓存过期,此时数万用户同时抢购,请求全部打到数据库查库存,可能直接压垮数据库。

产生原因
  1. 热点Key的缓存过期(如设置了较短的TTL,且过期瞬间有大量并发请求)。
  2. 热点Key在缓存中根本不存在(如首次访问,缓存未预热),且被高频访问。
解决方案

核心思路:在热点Key失效瞬间,控制对数据库的并发请求,避免“一窝蜂”访问

  1. 互斥锁(分布式锁)

    • 当缓存失效时,先尝试获取分布式锁(如Redis的SETNX),只有获取到锁的请求才能访问数据库,其他请求等待或重试。
    • 数据库查询完成后,更新缓存,再释放锁。后续请求即可从缓存获取数据。
    • 示例(伪代码):
      public string GetHotData(string key) {
          // 1. 先查缓存
          string data = redis.Get(key);
          if (data != null) return data;
      
          // 2. 缓存失效,尝试获取分布式锁
          bool locked = redis.SetNX($"lock:{key}", "1", TimeSpan.FromSeconds(5)); // 锁超时避免死锁
          if (locked) {
              try {
                  // 3. 拿到锁,查数据库
                  data = db.QueryData(key);
                  // 4. 更新缓存(设置合理的过期时间)
                  redis.Set(key, data, TimeSpan.FromMinutes(10));
                  return data;
              } finally {
                  // 5. 释放锁
                  redis.Delete($"lock:{key}");
              }
          } else {
              // 6. 未拿到锁,休眠后重试(避免频繁请求)
              Thread.Sleep(100);
              return GetHotData(key); // 递归重试
          }
      }
      
  2. 热点Key永不过期

    • 对热点Key不设置过期时间(物理不过期),但通过业务代码手动更新缓存(如定时任务)。
    • 适用于热点数据更新频率低的场景(如静态配置、热门商品基本信息)。
  3. 提前预热 + 过期时间错开

    • 系统启动时,主动将热点Key加载到缓存(预热),避免首次访问穿透。
    • 对同一批热点Key设置随机的过期时间(如基础过期时间±10%),避免同时失效。

二、缓存雪崩(Cache Avalanche)

定义

缓存雪崩是指:大量缓存Key在同一时间失效,或缓存服务器(如Redis集群)整体宕机,导致所有请求全部穿透到数据库,数据库因瞬间压力过大而崩溃

例如:电商平台在凌晨0点对所有商品缓存设置了24小时过期,次日0点所有商品缓存同时失效, millions级请求直接打到数据库,导致数据库宕机。

产生原因
  1. 大量Key集中过期:缓存设置了相同的过期时间(如批量更新时统一设置TTL),到期后同时失效。
  2. 缓存服务不可用:缓存集群宕机(如Redis主从切换失败、网络故障),导致所有缓存查询失败。
解决方案

核心思路:避免大量Key同时失效,提高缓存服务可用性,降低数据库压力

  1. 过期时间随机化

    • 对缓存Key的过期时间添加随机值(如基础TTL + 随机数(0~300秒)),避免集中过期。
    • 示例:
      // 原过期时间:1小时,改为1小时±5分钟
      int baseExpire = 3600;
      int random = new Random().Next(0, 300);
      int actualExpire = baseExpire + random; // 3600~3900秒
      redis.Set(key, data, TimeSpan.FromSeconds(actualExpire));
      
  2. 缓存集群高可用

    • 部署缓存集群(如Redis主从+哨兵、Redis Cluster),避免单点故障。即使部分节点宕机,其他节点仍可提供服务。
    • 配置自动故障转移(如哨兵模式自动切换主节点),减少服务中断时间。
  3. 多级缓存

    • 引入本地缓存(如C#中的MemoryCache)+ 分布式缓存(如Redis)的多级架构。
    • 当分布式缓存失效时,先查本地缓存,减少对数据库的直接冲击。
  4. 熔断降级

    • 使用熔断工具(如Sentinel、Hystrix)监控数据库压力,当请求量超过阈值时,自动“熔断”对数据库的访问,返回默认值(如“系统繁忙,请稍后再试”)。
    • 避免数据库被压垮,保障核心服务可用。
  5. 缓存预热与降级开关

    • 提前将热点数据加载到缓存(预热),减少缓存失效的概率。
    • 配置降级开关:当缓存不可用时,临时关闭非核心功能(如商品详情页的推荐模块),减少请求量。

二、缓存穿透(Cache Penetration)

在缓存系统中,缓存穿透(Cache Penetration) 是指:查询一个“根本不存在的数据”(缓存和数据库中都没有该数据),导致每次请求都无法命中缓存,直接穿透到数据库。由于缓存无法拦截这类请求,若存在大量此类查询(如恶意攻击),会导致数据库持续接收无效请求,压力骤增甚至宕机。

1、缓存穿透的典型场景
  • 恶意攻击:黑客伪造大量不存在的Key(如随机生成的用户ID、订单号)发起高频查询,由于缓存和数据库均无对应数据,所有请求直接打向数据库。
  • 业务误操作:例如,某商品数据被误删除,而前端仍在持续请求该商品详情,导致每次查询都穿透到数据库。
  • 新数据未同步:新生成的数据尚未写入缓存和数据库(如刚创建的订单还未落地),此时查询会穿透。
2、产生原因
  1. 缓存和数据库中均无该数据:请求的Key是“无效的”(如不存在的ID、非法参数),缓存无法命中,数据库查询结果也为空。
  2. 高频访问无效Key:此类无效请求被大量、持续发起,缓存无法“屏蔽”(因为缓存中没有对应Entry),导致数据库持续处理无效查询。
3、解决方案

核心思路:拦截无效请求,避免其穿透到数据库,或通过缓存“空结果”减少数据库访问。

(1). 缓存空值(Cache Null)

当数据库查询结果为空时,将“空值”(或特殊标记,如null"")存入缓存,并设置较短的过期时间(如1~5分钟)。后续请求会命中缓存的空值,无需再访问数据库。

示例(伪代码)

public string GetData(string key) {
    // 1. 先查缓存
    string data = redis.Get(key);
    if (data != null) {
        // 缓存命中:若为特殊空标记,返回空;否则返回数据
        return data == "NULL_MARKER" ? null : data;
    }

    // 2. 缓存未命中,查数据库
    data = db.QueryData(key); // 数据库查询结果为null(数据不存在)
    if (data == null) {
        // 3. 缓存空值(设置短过期,如1分钟,避免长期占用缓存)
        redis.Set(key, "NULL_MARKER", TimeSpan.FromMinutes(1)); 
        return null;
    }

    // 4. 数据库有数据,正常缓存并返回
    redis.Set(key, data, TimeSpan.FromHours(1));
    return data;
}

优缺点

  • 优点:实现简单,能快速拦截重复的无效请求。
  • 缺点:
    • 缓存空值会占用缓存空间(若无效Key极多,可能导致缓存膨胀);
    • 过期时间需合理设置(太短则无法有效拦截,太长则可能影响新数据写入后的查询)。
(2). 布隆过滤器(Bloom Filter)

布隆过滤器是一种空间效率极高的概率性数据结构,用于快速判断“一个元素是否在集合中”。它的核心思想是:预先将所有“可能存在的有效Key”存入布隆过滤器,查询时先通过布隆过滤器判断Key是否可能存在——若不存在,直接返回;若可能存在,再走“缓存→数据库”流程。

工作流程

  1. 预热阶段:将数据库中所有有效Key(如用户ID、商品ID)存入布隆过滤器。
  2. 查询阶段
    • 接收请求Key,先通过布隆过滤器判断:若布隆过滤器判定“不存在”,直接返回空(无需访问缓存和数据库);
    • 若布隆过滤器判定“可能存在”,再查询缓存→数据库(正常流程)。

示例(伪代码)

// 初始化布隆过滤器(预先加载所有有效Key)
private BloomFilter<string> _bloomFilter = new BloomFilter<string>(expectedElements: 1000000, falsePositiveRate: 0.01);

public string GetData(string key) {
    // 1. 先过布隆过滤器:若不存在,直接返回
    if (!_bloomFilter.Contains(key)) {
        return null;
    }

    // 2. 布隆过滤器判定可能存在,再查缓存
    string data = redis.Get(key);
    if (data != null) {
        return data;
    }

    // 3. 缓存未命中,查数据库
    data = db.QueryData(key);
    if (data != null) {
        redis.Set(key, data, TimeSpan.FromHours(1));
    }
    return data;
}

优缺点

  • 优点:
    • 空间效率极高(存储100万Key仅需几MB内存);
    • 查询速度快(O(1)时间复杂度),能有效拦截绝大多数无效请求。
  • 缺点:
    • 存在误判率(可能将不存在的Key判定为“可能存在”,但不会漏判),误判率可通过参数调整(误判率越低,占用空间越大);
    • 不支持删除操作(删除Key会影响其他元素的判断),适合Key相对稳定的场景(如用户ID、商品SKU)。
(3). 接口层限流与参数校验
  • 限流:对接口设置访问频率限制(如用令牌桶算法),防止恶意请求洪水(如每秒最多1000次请求),即使存在穿透,也能控制数据库压力。
  • 参数校验:在接口层对请求参数进行合法性校验,过滤明显无效的Key(如用户ID为负数、订单号格式错误),从源头拦截无效请求。

示例

// 接口限流(使用ASP.NET Core的RateLimiter)
[EnableRateLimiting("fixed")] // 应用限流策略(如每分钟最多100次)
public IActionResult GetProduct(string productId) {
    // 参数校验:过滤无效ID
    if (!IsValidProductId(productId)) { // 自定义校验逻辑(如格式、范围)
        return BadRequest("无效的商品ID");
    }
    // 正常业务逻辑...
}
(4). 数据预热与兜底策略
  • 数据预热:系统启动时,将所有有效Key提前加载到缓存和布隆过滤器,减少“新Key未同步”导致的穿透。
  • 兜底返回:对于无法通过布隆过滤器或参数校验拦截的请求,返回兜底数据(如“数据不存在”),避免直接访问数据库。

四、缓存穿透 vs 缓存击穿 vs 缓存雪崩

场景核心原因解决方案核心思路
缓存穿透查询不存在的Key,缓存和数据库均无数据缓存空值、布隆过滤器、参数校验
缓存击穿热点Key失效,大量并发请求穿透分布式锁、热点Key永不过期
缓存雪崩大量Key集中失效或缓存服务宕机随机过期时间、缓存集群高可用、熔断降级
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

YuanlongWang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值