Redis 高可用架构实战:缓存穿透、击穿、雪崩的全面防御

小小卒子2026-10-20后端⏱ 5min

Redis 高可用架构实战:缓存穿透、击穿、雪崩的全面防御

Redis 几乎是现代后端缓存的事实标准。但在生产环境中,"缓存三兄弟"——穿透、击穿、雪崩——是压垮系统的高频元凶。本文从问题本质出发,给出可落地的防御方案与高可用架构演进路径。

一、缓存穿透:查询不存在的数据

现象:请求的数据在缓存和数据库都不存在,每次请求都直达数据库。攻击者利用不存在的 key(如负 ID)可轻易打垮数据库。

防御一:缓存空值

func GetUser(id int64) (*User, error) {
    key := fmt.Sprintf("user:%d", id)
    if v, err := rdb.Get(ctx, key).Result(); err == nil {
        if v == "" { return nil, ErrNotFound } // 命中空值缓存
        var u User
        json.Unmarshal([]byte(v), &u)
        return &u, nil
    }
    u, err := db.GetUser(id)
    if errors.Is(err, ErrNotFound) {
        rdb.Set(ctx, key, "", 5*time.Minute) // 空值也缓存,TTL 短一点
        return nil, ErrNotFound
    }
    ...
}

防御二:布隆过滤器

在缓存前加一层 Bloom Filter,不存在的数据直接短路:

// 初始化:全量用户 ID 写入布隆过滤器
bf := bloom.NewWithEstimates(1_000_000, 0.001)
for id := range allUserIDs() { bf.Add(id) }

func GetUser(id int64) (*User, error) {
    if !bf.Test(id) { return nil, ErrNotFound } // 一定不存在,直接返回
    // 再走缓存 -> 数据库
}

二、缓存击穿:热点 key 过期瞬间

现象:某个热点 key 恰好过期,高并发请求同时打到数据库。

防御一:互斥锁(只让一个请求回源)

func GetHot(key string, fn func() (any, error)) (any, error) {
    if v, err := rdb.Get(ctx, key).Result(); err == nil {
        return v, nil
    }
    lock := rdb.SetNX(ctx, "lock:"+key, "1", 3*time.Second)
    if lock.Val() {
        defer rdb.Del(ctx, "lock:"+key)
        v, err := fn()                 // 只有一个线程回源
        rdb.Set(ctx, key, marshal(v), 10*time.Minute)
        return v, err
    }
    time.Sleep(50 * time.Millisecond)  // 其他请求短暂等待
    return GetHot(key, fn)             // 重试读缓存
}

防御二:逻辑过期(永不过期 + 后台刷新)

缓存不设物理过期,value 中带过期时间戳,读到时判断逻辑过期后异步重建:

type CacheItem struct {
    Value     string `json:"value"`
    ExpiredAt int64  `json:"expired_at"`
}

func Get(key string, fn func() (any, error)) (any, bool) {
    item := getFromRedis(key)
    if time.Now().Unix() < item.ExpiredAt {
        return item.Value, true // 未过期,直接返回
    }
    // 逻辑过期:返回旧值,同时异步重建
    go func() {
        v, _ := fn()
        setWithLogicalTTL(key, v, 10*time.Minute)
    }()
    return item.Value, true
}

三、缓存雪崩:大量 key 同时失效

现象:缓存中大量 key 在同一时刻过期,请求全部打向数据库;或 Redis 实例宕机导致缓存整体不可用。

防御一:过期时间加随机抖动

ttl := baseTTL + time.Duration(rand.Intn(600))*time.Second
rdb.Set(ctx, key, v, ttl)

防御二:多级缓存兜底

Redis 之上加本地缓存(Caffeine / freecache),命中本地缓存直接返回,Redis 挂掉时仍有二级兜底。

防御三:限流与熔断

回源数据库前加 Semaphore 限流,超阈值直接降级返回旧数据:

if sem.TryAcquire() {
    defer sem.Release()
    v, err := db.Query(...)  // 回源
    cache(key, v)
    return v
}
return fallbackData // 降级:返回旧缓存 / 默认值

四、Redis 高可用架构演进

4.1 主从复制 + 哨兵(Sentinel)

  • 一主多从,读写分离,主节点故障自动切换;
  • Sentinel 负责监控、通知、自动故障转移;
  • 解决"单点"问题,但主从切换瞬间可能有短暂写不可用。

4.2 集群模式(Cluster)

  • 16384 个 hash slot 分布在多个主节点,数据自动分片;
  • 每个主节点带从节点,保证分片高可用;
  • 适合数据量超单机内存、需要水平扩展的场景;
  • 注意:Cluster 模式不支持多 key 事务、KEYS、MGET 跨 slot 操作。

4.3 持久化策略取舍

| 策略 | 机制 | 优点 | 风险 | |------|------|------|------| | RDB | 定期快照 | 恢复快、文件小 | 可能丢失最近数据 | | AOF | 追加写日志 | 数据完整(appendfsync everysec) | 文件大、恢复慢 | | 混合 | RDB + AOF | 兼顾恢复速度与完整性 | 配置略复杂 |

生产建议:AOF 开启,appendfsync everysec,必要时搭配 RDB 做冷备。

五、常见缓存一致性方案

  • Cache Aside(旁路缓存):读 miss 回源写缓存;更新时先更新 DB 再删缓存,是多数业务的首选;
  • 延迟双删:先删缓存、更新 DB、稍后(如 500ms)再删一次,规避并发下的脏读窗口;
  • 监听 binlog 异步更新:通过 Canal 订阅 MySQL binlog,增量同步到 Redis,彻底解耦业务代码。

结语

缓存系统的核心不是"用不用 Redis",而是想清楚每一层缓存的生命周期与失败模式。穿透、击穿、雪崩本质上都是"缓存不可用"的具体形态:把空值、锁、随机 TTL、多级缓存、限流降级这五板斧用对位置,系统就能在极端流量下依然稳如磐石。

读完了?给作者一点支持

本文采用 CC BY 4.0 协议转载 · © 小卒子

评论区加载中…

🔗 相关文章