Skip to content

Redis-缓存(穿透、击穿、雪崩)

缓存穿透

问题定义

缓存穿透是指查询不存在的数据,导致请求绕过缓存直接访问底层存储系统(如数据库)的现象。与缓存击穿/雪崩的关键区别在于请求的是系统中根本不存在的资源

典型场景

  • 恶意攻击:故意构造大量不存在的ID发起请求
  • 业务缺陷:爬虫抓取超出范围的页面ID
  • 数据淘汰:已删除数据仍被频繁请求

系统危害

mermaid
graph LR
    A[大量非法请求] --> B[缓存层失效]
    B --> C[数据库压力激增]
    C --> D[连接池耗尽]
    D --> E[系统整体崩溃]

解决方案

布隆过滤器(Bloom Filter)

  • 实现原理

    • 位数组 + k个哈希函数
    • 空间效率极高的概率型数据结构
    • 特性:
      • 若判定"不存在"则100%准确
      • 若判定"存在"可能有误判(可控制)
  • Redis实现

    bash
    # 使用RedisBloom模块(推荐)
    > BF.RESERVE myFilter 0.001 1000000  # 创建过滤器(0.1%误判率,100万容量)
    > BF.ADD myFilter validKey1          # 添加合法键
    > BF.EXISTS myFilter invalidKey      # 检查键是否存在(返回0表示肯定不存在)
    
    # 原生Redis模拟实现(String+位操作)
    > SETBIT bf 12345 1    # 模拟哈希函数1
    > SETBIT bf 67890 1    # 模拟哈希函数2
    > GETBIT bf 12345      # 检查位
  • 优化技巧

    • 动态扩容过滤器
    • 结合多个小过滤器降低误判率

空值缓存

  • 实现方案
java
// 伪代码示例
public Object getData(String key) {
    Object value = redis.get(key);
    if (value == null) {
        if (redis.exists("null_" + key)) {  // 检查空值标记
            return null;
        }
        value = db.get(key);
        if (value == null) {
            redis.setex("null_" + key, 300, "");  // 空值缓存5分钟
        } else {
            redis.setex(key, 3600, value);  // 正常缓存1小时
        }
    }
    return value;
}
  • 注意事项
    • 设置合理的短TTL(通常5-30分钟)
    • 定期清理历史空值键
    • 对恶意攻击可结合IP限流

数据预热

Redis服务启动时加载热点数据到布隆过滤器。

缓存击穿

问题定义

缓存击穿(Cache Breakdown)是指在高并发场景下,当热点数据缓存过期的瞬间,大量请求同时穿透缓存直接访问数据库的现象。与缓存穿透(查询不存在的数据)和缓存雪崩(大量缓存同时失效)不同,缓存击穿特指单个热点key失效引发的数据库压力激增问题。

典型场景

  • 电商秒杀:热门商品库存信息缓存过期时,瞬时数万查询涌向数据库
  • 新闻热点:突发新闻内容缓存失效后,大量用户同时刷新页面
  • 社交feed流:明星动态缓存过期引发粉丝集体访问

系统危害

mermaid
graph LR
    A[热点key失效] --> B[万级QPS穿透缓存]
    B --> C[数据库连接池耗尽]
    C --> D[数据库响应延迟飙升]
    D --> E[服务雪崩]

解决方案

互斥锁

  • 核心思想:通过分布式锁确保只有一个请求能重建缓存,其他请求等待或重试。

  • Redis分布式锁实现

    bash
    # 使用SETNX实现锁(推荐Redisson框架)
    SET lock_key unique_value NX PX 3000  # 获取3秒锁
  • 完整流程

mermaid
graph TD
    A[获取缓存] --> B{存在?}
    B -->|是| C[返回数据]
    B -->|否| D[获取分布式锁]
    D --> E{获锁成功?}
    E -->|是| F[查询DB]
    F --> G[重建缓存]
    G --> H[释放锁]
    E -->|否| I[短暂睡眠后重试]
  • 优化要点
    • 锁超时时间要短于业务超时时间
    • 必须设置唯一value防误删
    • 推荐使用Redisson的看门狗机制

逻辑过期

  • 数据结构设计

    json
    {
      "value": "真实数据",
      "expire": 1672500000  // 逻辑过期时间戳
    }
  • 处理流程

    python
    def get_data(key):
        data = redis.get(key)
        if data is None:
            return load_db_data(key)  # 首次加载
        
        if data['expire'] > time.now():
            return data['value']
        else:
            if acquire_lock(key):  # 获取更新锁
                try:
                    new_data = load_db_data(key)
                    redis.set(key, pack_data(new_data))
                finally:
                    release_lock(key)
            return data['value']  # 返回旧数据
  • 优势

    • 不阻塞请求
    • 保证数据最终一致性

热点数据永不过期

实现方案

  1. 不设置TTL,通过后台定时任务定期更新
  2. 采用延迟双删策略:
    bash
    redis.del(key)          # 第一次删除
    db.update()            # 更新数据库
    sleep(500ms)           # 等待主从同步
    redis.del(key)          # 第二次删除

缓存预热

实施步骤

  1. 热点发现:通过实时监控(如Redis的hotkeys命令)识别热点key
  2. 预加载机制
    java
    @Scheduled(fixedRate = 5 * 60 * 1000) // 每5分钟执行
    public void preloadHotData() {
        List<String> hotKeys = monitor.getHotKeys();
        hotKeys.forEach(key -> {
            Object data = db.query(key);
            redis.set(key, data);
        });
    }
    最佳实践:在业务低峰期执行预热

监控指标

指标名称报警阈值监控工具
缓存击穿次数>50次/分钟Prometheus
数据库QPS突增>300%基线Grafana
分布式锁等待时间>500msELK日志分析

缓存雪崩

问题定义

缓存雪崩是指大量缓存数据在同一时间失效缓存服务整体宕机,导致所有请求直接涌向数据库,造成数据库压力激增甚至崩溃的连锁反应现象。与缓存击穿(单个热点key失效)和缓存穿透(查询不存在数据)不同,缓存雪崩的特点是影响范围大、系统级风险高

典型场景

  • 定时任务刷新缓存:凌晨批量更新缓存时设置了相同的过期时间
  • 缓存服务重启:Redis集群整体重启导致所有缓存丢失
  • 突发流量冲击:热点数据集中失效引发数据库查询风暴

系统级连锁反应

mermaid
graph TD
    A[大量缓存同时失效] --> B[万级QPS直连数据库]
    B --> C[数据库连接池耗尽]
    C --> D[数据库响应延迟飙升]
    D --> E[应用服务线程阻塞]
    E --> F[系统全面崩溃]

解决方案

过期时间分散化(基础防护)

原理:通过为不同key设置随机过期时间偏移量,避免集中失效。

实现方案

java
// Java示例:基础TTL+随机偏移
int baseTTL = 3600; // 1小时基础过期
int randomTTL = new Random().nextInt(300); // 0-5分钟随机
redisTemplate.opsForValue().set(key, value, baseTTL + randomTTL, TimeUnit.SECONDS);

最佳实践

  • 热点数据:±5%随机偏移(如1小时±3分钟)
  • 普通数据:±20%随机偏移(如1小时±12分钟)

多级缓存架构(立体防御)

分层设计

mermaid
graph BT
    A[客户端] --> B[浏览器缓存]
    A --> C[CDN缓存]
    A --> D[Nginx缓存]
    D --> E[本地缓存]
    E --> F[Redis集群]
    F --> G[数据库]

各级配置建议

缓存层级技术实现TTL策略承载量级
浏览器Cookie/Storage60s固定万级QPS
CDN边缘节点300s固定百万级QPS
本地缓存Caffeine30s±10s十万级QPS
RedisCluster1h±5min百万级QPS

高可用集群部署(基础设施)

Redis集群方案对比

方案特点适用场景故障恢复时间
主从+哨兵配置简单中小规模系统10-30秒
Redis Cluster自动分片大规模系统秒级切换
多活架构跨机房容灾金融级系统毫秒级切换

熔断降级机制(应急防护)

三级防护体系

  1. 限流

    yaml
    # Spring Cloud Gateway配置示例
    filters:
      - name: RequestRateLimiter
        args:
          redis-rate-limiter.replenishRate: 1000 # 每秒令牌数
          redis-rate-limiter.burstCapacity: 2000 # 峰值容量
  2. 熔断

    java
    // Hystrix配置示例
    @HystrixCommand(
      fallbackMethod = "getDefaultData",
      commandProperties = {
        @HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="20"),
        @HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="5000")
      }
    )
  3. 降级

    • 核心数据:返回本地缓存或静态数据
    • 非核心数据:直接返回"服务繁忙"提示

热点数据永不过期+异步更新(特殊场景)

实现方案

python
def get_data(key):
    data = redis.get(key)
    if data['expire'] < time.time():  # 逻辑过期检查
        mq.publish('cache_update', key)  # 触发异步更新
    return data['value']

# 消费者处理
def update_cache(key):
    with distributed_lock(key):  # 分布式锁
        new_data = db.query(key)
        redis.set(key, {
            'value': new_data,
            'expire': time.time() + 3600  # 重置1小时
        })

适用场景

  • 商品详情页
  • 秒杀活动数据
  • 实时排行榜