1. 什么是缓存雪崩?真实场景的具象化理解

某电商平台凌晨0点开启双11秒杀活动,后台系统为减轻数据库压力,将商品库存数据缓存在Redis中,所有缓存Key都设置了1小时固定过期时间。当0:55分时,100万用户同时刷新页面,此时缓存集群中80%的Key同时失效,导致海量请求直接穿透到数据库,MySQL连接池瞬间被打满,整个服务瘫痪。

这种因大量缓存集中失效导致数据库压力激增的现象,就是我们常说的「缓存雪崩」。就像高峰期的地铁站突然所有闸机同时故障,导致乘客全部涌向人工通道。

2. 应急工具箱

2.1 随机过期时间方案(Redis+SpringBoot)
// 商品服务缓存设置示例
public void setProductCache(String productId, Product product) {
    // 基础过期时间30分钟
    int baseExpire = 1800; 
    // 随机浮动范围±300秒(5分钟)
    int randomRange = new Random().nextInt(600) - 300; 
    // 最终过期时间计算
    int finalExpire = baseExpire + randomRange;
    
    redisTemplate.opsForValue().set(
        "product:" + productId, 
        product,
        finalExpire, 
        TimeUnit.SECONDS
    );
}

关键技术点说明:

  • 基础过期时间设定业务容忍的最低缓存周期
  • 随机浮动范围建议控制在基础时间的10%-20%
  • 使用ThreadLocalRandom替代Random可获得更好性能
2.2 热点数据永不过期方案(Redis+Lua)
-- 商品库存查询脚本
local key = KEYS[1]
local stock = redis.call('GET', key)
if not stock then
    -- 从数据库加载数据
    stock = loadFromDB(key) 
    -- 设置永久缓存(实际可设较长过期时间)
    redis.call('SET', key, stock)
end
return stock

-- 定时更新脚本(每小时执行)
local keys = redis.call('KEYS', 'product:*')
for _,k in ipairs(keys) do
    local newStock = getLatestStockFromDB(k)
    redis.call('SET', k, newStock)
end

方案特点:

  • 消除集中过期风险
  • 需要配套异步更新机制
  • 建议对冷数据设置兜底过期时间

3. 熔断降级(Sentinel+Redis)

// 基于Sentinel的熔断降级配置
@SentinelResource(
    value = "productQuery",
    fallback = "queryProductFallback",
    blockHandler = "queryProductBlock",
    exceptionsToIgnore = {IllegalArgumentException.class}
)
public Product queryProduct(String productId) {
    // 正常业务逻辑
}

// 降级方法(返回兜底数据)
private Product queryProductFallback(String productId, Throwable ex) {
    return new Product().setDefaultData();
}

// 流量控制规则配置
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("productQuery");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(500); // 阈值设为500QPS
rules.add(rule);
FlowRuleManager.loadRules(rules);

熔断策略组合:

  • 慢调用比例(>800ms请求占比超50%)
  • 异常比例(错误率超过60%)
  • 异常数(5分钟内异常超过100次)

4. 多级缓存架构设计(Redis+本地缓存)

// 多级缓存加载逻辑示例
public Product getProduct(String productId) {
    // 第一级:本地缓存
    Product product = localCache.get(productId);
    if (product != null) return product;
    
    // 第二级:分布式锁防击穿
    RLock lock = redisson.getLock("lock:" + productId);
    try {
        lock.lock();
        // 双重检查
        product = localCache.get(productId);
        if (product != null) return product;
        
        // 第三级:Redis缓存
        product = redisTemplate.opsForValue().get("product:" + productId);
        if (product != null) {
            localCache.put(productId, product);
            return product;
        }
        
        // 终极回源:数据库查询
        product = db.queryProduct(productId);
        redisTemplate.opsForValue().set("product:"+productId, product, 30, TimeUnit.MINUTES);
        localCache.put(productId, product);
        return product;
    } finally {
        lock.unlock();
    }
}

多级缓存注意事项:

  • 本地缓存建议使用Caffeine或Guava Cache
  • 需要处理缓存一致性问题
  • 建议设置本地缓存上限

5. 灾后快速恢复手册

步骤一:服务降级 立即启用静态兜底数据,暂时关闭非核心功能:

location /product {
    # 正常服务路径
    proxy_pass http://backend;
    
    # 熔断时切换
    error_page 502 503 504 = @fallback;
}

location @fallback {
    root /static/fallback;
    try_files /product_default.html =404;
}

步骤二:渐进式重建 使用限流工具逐步重建缓存:

# 使用redis-cli批量设置过期时间
redis-cli -h 127.0.0.1 -p 6379 --scan --pattern 'product:*' | \
xargs -I{} redis-cli -h 127.0.0.1 -p 6379 EXPIRE {} 3600

# 使用管道加速写入
cat product_data.txt | redis-cli --pipe

6. 应用场景与技术选型

典型应用场景

  • 电商大促活动
  • 新闻热点事件
  • 定时任务触发的缓存刷新
  • 集群批量重启场景

技术方案对比

方案 响应延迟 实现复杂度 数据一致性 适用场景
随机过期时间 简单 最终一致 常规业务场景
永不过期+异步更新 最低 复杂 强一致 高频访问热点数据
多级缓存 极低 较复杂 最终一致 高并发读场景

7. 经验总结与避坑指南

  1. 监控指标三要素

    • 缓存命中率低于90%时触发预警
    • 数据库QPS突增50%立即告警
    • Redis集群连接数使用率超过75%扩容
  2. 容量规划黄金法则

    所需Redis内存 = (单品缓存大小 × 日均UV × 缓存周期天数) × 安全系数(1.5)
    
  3. 避坑实践

    • 避免使用KEYS命令扫描大数据量
    • 批量操作使用pipeline提升10倍性能
    • 集群模式每个分片保留15%-20%内存余量