当前位置:首页 > 问答 > 正文

Redis缓存 缓存击穿防护机制Redis缓存中加锁应对击穿攻击的方法解析

🔥 Redis缓存击穿防护:用「锁」挡住流量暴击的终极指南

场景:黑色星期五的惊魂夜 🛒

凌晨3点,电商团队的小王盯着监控屏幕冷汗直流——某爆款商品页面突然瘫痪,数据库CPU飙到100%,原来这个价值999元的扫地机器人库存只剩最后1件,每秒10万次的查询直接击穿了Redis缓存,所有请求像洪水般涌向数据库...这就是典型的缓存击穿现场!


什么是缓存击穿?💥

某个热点key过期瞬间,海量请求直接穿透缓存砸向数据库(就像子弹击穿防弹衣),与缓存穿透(查询不存在的数据)不同,击穿针对的是真实存在但暂时失效的热点数据

典型特征

  • 📌 单个key突然失效
  • 📌 该key对应极高并发请求
  • 📌 数据库出现短期过载

加锁方案全景图 🔐

方案1:互斥锁(Mutex Lock)

def get_data(key):
    data = redis.get(key)
    if data is None:  # 缓存未命中
        if redis.setnx("lock:" + key, 1, ex=5):  # 加锁
            data = db.query(key)          # 查数据库
            redis.set(key, data, ex=300)  # 重建缓存
            redis.delete("lock:" + key)   # 释放锁
        else:
            time.sleep(0.1)  # 短暂等待
            return get_data(key)  # 重试
    return data

优点:实现简单,保证强一致性
缺点:存在死锁风险(需设置锁超时)

Redis缓存 缓存击穿防护机制Redis缓存中加锁应对击穿攻击的方法解析


方案2:逻辑过期(逻辑TTL)

// 缓存值结构:{"data":真实数据, "expire":逻辑过期时间}
String getDataWithLogicExpire(String key) {
    String json = redis.get(key);
    if (json != null) {
        CacheObj cacheObj = parseJson(json);
        if (cacheObj.expire > System.currentTimeMillis()) {
            return cacheObj.data;  // 未过期直接返回
        }
        if (redis.setnx("lock:" + key, "1")) {  // 获取重建权
            new Thread(() -> {
                String newData = db.query(key);  // 异步重建
                redis.set(key, new CacheObj(newData, 30*60*1000));
                redis.del("lock:" + key);
            }).start();
        }
    }
    return cacheObj.data;  // 返回旧数据
}

适用场景:允许短期数据不一致的电商类目等


方案3:分布式锁进阶版 🚀

func GetProductDetail(productID string) (string, error) {
    // 1. 尝试获取缓存
    cacheVal, err := redis.Get(ctx, productID).Result()
    if err != nil && err != redis.Nil {
        return "", err
    }
    // 2. 缓存命中
    if cacheVal != "" {
        return cacheVal, nil
    }
    // 3. 获取分布式锁(RedLock算法)
    lockKey := fmt.Sprintf("lock:%s", productID)
    mutex := redsync.New(redisPool).NewMutex(lockKey, redsync.WithExpiry(10*time.Second))
    if err := mutex.Lock(); err != nil {
        time.Sleep(50 * time.Millisecond)
        return GetProductDetail(productID) // 递归重试
    }
    defer mutex.Unlock()
    // 4. 二次检查(Double Check)
    cacheVal, _ = redis.Get(ctx, productID).Result()
    if cacheVal != "" {
        return cacheVal, nil
    }
    // 5. 数据库查询
    dbData, err := db.QueryProductDetail(productID)
    if err != nil {
        return "", err
    }
    // 6. 写入缓存(设置随机过期时间防雪崩)
    rand.Seed(time.Now().UnixNano())
    expire := 300 + rand.Intn(60) // 300~360秒随机
    redis.Set(ctx, productID, dbData, time.Duration(expire)*time.Second)
    return dbData, nil
}

技术要点

  • 🛡️ 采用RedLock防止单点故障
  • 🔍 Double Check避免重复计算
  • 🎲 随机过期时间预防雪崩

方案选型决策树 🌳

         开始
           │
           ▼
是否需要强一致性?─────┬─────→ 是 → 使用互斥锁方案
           │               (金融交易等场景)
           ↓ 否
能否接受短暂旧数据?──┬──→ 能 → 逻辑过期方案
           │               (商品信息展示)
           ↓ 不能
使用分布式锁+双重检查
(秒杀/库存等场景)

避坑指南 ⚠️

  1. 锁超时陷阱
    锁过期时间要大于业务处理时间+网络延迟,否则会出现:

    • 线程A未处理完锁已过期
    • 线程B获得锁开始处理
    • 线程A完成操作误删线程B的锁
  2. 雪崩预防
    对热点key采用分级缓存策略

    • L1:本地缓存(10秒TTL)
    • L2:Redis缓存(5分钟TTL)
    • 数据库:最终数据源
  3. 监控指标
    必须配置以下告警:

    Redis缓存 缓存击穿防护机制Redis缓存中加锁应对击穿攻击的方法解析

    • 🔔 缓存击穿次数(cache_penetration_count
    • 🔔 锁等待时间(lock_wait_duration
    • 🔔 缓存重建失败率

2025年新动向 🆕

根据2025年Redis官方社区报告,两种新兴方案开始流行:

  1. Tair混合持久化
    阿里云推出的「内存+持久化」混合模式,热点key自动持久化,重启后立即恢复

  2. Redis 7.4版原子化操作
    新增GETEX命令组合查询与延期操作:

    GETEX key EX 3600  # 查询并自动续期

🛡️

缓存击穿就像系统血管中的"血栓",而合理的锁机制就是最好的"溶栓剂"。没有万能的银弹,根据你的业务特性(一致性要求、并发量、容忍延迟)选择最适合的方案才是王道,下次遇到流量洪峰时,希望你能淡定地祭出合适的锁方案,让数据库继续"躺平"!

发表评论