Redis缓存穿透与雪崩防护:布隆过滤器与多级缓存架构设计

Redis缓存策略是高并发架构中提升响应速度的核心手段,但缓存引入后也带来了新的问题:缓存穿透、缓存击穿、缓存雪崩。这三种异常场景处理不当会导致大量请求直达数据库,引发性能雪崩。本文从问题诊断到解决方案,完整演示Redis缓存防护体系的搭建过程。

三种缓存异常场景区分

场景 触发条件 危害
缓存穿透 查询不存在的数据,缓存和DB都没有 每次请求都打到DB
缓存击穿 热点Key过期瞬间,大量并发请求 瞬间DB压力激增
缓存雪崩 大量Key同时过期或Redis宕机 DB被压垮,服务不可用

缓存穿透:布隆过滤器方案

缓存穿透的典型场景是恶意攻击——用大量不存在的ID发起查询,绕过缓存直接打数据库。MySQL性能调优无法解决这类问题,必须在缓存层拦截。

布隆过滤器是一种空间效率极高的概率型数据结构,能判断一个元素”一定不存在”或”可能存在”。误判率可控,不会漏判(不存在的不会误报存在)。

package cache

import (
    "context"
    "errors"
    "time"

    "github.com/redis/go-redis/v9"
    "github.com/bits-and-blooms/bloom/v3"
)

// BloomFilter 基于Redis的分布式布隆过滤器
type BloomFilter struct {
    rdb    *redis.Client
    key    string
    filter *bloom.BloomFilter // 本地缓存副本
}

// NewBloomFilter 创建布隆过滤器
// expectedItems: 预期元素数量
// falsePositiveRate: 期望误判率,如0.01表示1%
func NewBloomFilter(rdb *redis.Client, key string, expectedItems uint, falsePositiveRate float64) *BloomFilter {
    filter := bloom.NewWithEstimates(expectedItems, falsePositiveRate)
    return &BloomFilter{
        rdb:    rdb,
        key:    key,
        filter: filter,
    }
}

// Add 向布隆过滤器添加元素
func (bf *BloomFilter) Add(ctx context.Context, item string) error {
    // 写入Redis(使用SETBIT操作)
    // 同时写入本地缓存
    bf.filter.Add([]byte(item))
    
    // 实际项目中用Redis的SETBIT实现分布式布隆过滤器
    // 这里简化演示
    return nil
}

// Exists 判断元素是否存在
func (bf *BloomFilter) Exists(ctx context.Context, item string) (bool, error) {
    // 本地判断
    exists := bf.filter.Test([]byte(item))
    return exists, nil
}

// CacheService 带防护的缓存服务
type CacheService struct {
    rdb       *redis.Client
    bloom     *BloomFilter
}

func NewCacheService(rdb *redis.Client) *CacheService {
    bf := NewBloomFilter(rdb, "bloom:users", 1000000, 0.01)
    return &CacheService{
        rdb:   rdb,
        bloom: bf,
    }
}

// GetUser 带穿透防护的查询
func (cs *CacheService) GetUser(ctx context.Context, userID string) (*User, error) {
    // 第一层:布隆过滤器检查
    exists, err := cs.bloom.Exists(ctx, userID)
    if err != nil {
        return nil, err
    }
    if !exists {
        // 布隆过滤器说不存在,一定不存在
        return nil, ErrUserNotFound
    }

    // 第二层:查Redis缓存
    val, err := cs.rdb.Get(ctx, "user:"+userID).Result()
    if err == nil {
        // 缓存命中
        return deserializeUser(val)
    }

    // 缓存未命中,查数据库
    user, err := dbQueryUser(userID)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            // 数据库也没有,缓存空值(短TTL)
            cs.rdb.Set(ctx, "user:"+userID, "", 5*time.Minute)
            return nil, ErrUserNotFound
        }
        return nil, err
    }

    // 回写缓存,加随机TTL防雪崩
    ttl := 30*time.Minute + time.Duration(randInt(0, 300))*time.Second
    cs.rdb.Set(ctx, "user:"+userID, serializeUser(user), ttl)

    return user, nil
}

缓存击穿:分布式锁方案

热点Key过期瞬间,大量并发请求同时穿透到数据库。分布式锁方案让只有一个请求查DB并回填缓存,其他请求等待。

package cache

import (
    "context"
    "errors"
    "time"

    "github.com/redis/go-redis/v9"
    "github.com/google/uuid"
)

type HotKeyCache struct {
    rdb *redis.Client
}

// GetWithLock 使用分布式锁防止缓存击穿
func (hkc *HotKeyCache) GetWithLock(ctx context.Context, key string,
    loader func() (string, error)) (string, error) {

    // 先查缓存
    val, err := hkc.rdb.Get(ctx, key).Result()
    if err == nil && val != "" {
        return val, nil
    }

    // 缓存未命中,尝试获取分布式锁
    lockKey := "lock:" + key
    lockValue := uuid.New().String()

    // 尝试加锁,设置过期时间防止死锁
    locked, err := hkc.rdb.SetNX(ctx, lockKey, lockValue, 10*time.Second).Result()
    if err != nil {
        return "", err
    }

    if locked {
        // 获取锁成功,执行查询
        defer hkc.releaseLock(ctx, lockKey, lockValue)

        // double-check:防止前一个锁持有者已经写入缓存
        val, err := hkc.rdb.Get(ctx, key).Result()
        if err == nil && val != "" {
            return val, nil
        }

        // 查数据库
        result, err := loader()
        if err != nil {
            return "", err
        }

        // 回写缓存,加随机TTL
        ttl := 30*time.Minute + time.Duration(randInt(0, 300))*time.Second
        hkc.rdb.Set(ctx, key, result, ttl)

        return result, nil
    }

    // 获取锁失败,短暂等待后重试读缓存
    for i := 0; i < 3; i++ {
        time.Sleep(50 * time.Millisecond)
        val, err := hkc.rdb.Get(ctx, key).Result()
        if err == nil && val != "" {
            return val, nil
        }
    }

    // 重试失败,降级返回默认值或报错
    return "", ErrCacheMiss
}

// releaseLock 安全释放锁(Lua脚本保证原子性)
func (hkc *HotKeyCache) releaseLock(ctx context.Context, key, value string) {
    script := `
    if redis.call("get", KEYS[1]) == ARGV[1] then
        return redis.call("del", KEYS[1])
    else
        return 0
    end
    `
    hkc.rdb.Eval(ctx, script, []string{key}, value)
}

缓存雪崩:多级缓存与随机TTL

缓存雪崩的核心原因是大量Key同时过期。预防措施有两层:一是给TTL加随机抖动,避免同时失效;二是构建多级缓存,Redis挂掉时本地缓存兜底。

package cache

import (
    "context"
    "math/rand"
    "time"

    lru "github.com/hashicorp/golang-lru/v2"
)

// MultiLevelCache 多级缓存:本地缓存 → Redis → DB
type MultiLevelCache struct {
    localCache *lru.Cache[string, string]  // L1: 本地缓存
    rdb        *redis.Client                // L2: Redis
    localTTL   time.Duration
    redisTTL   time.Duration
}

func NewMultiLevelCache(rdb *redis.Client, localSize int) *MultiLevelCache {
    lc, _ := lru.New[string, string](localSize)
    return &MultiLevelCache{
        localCache: lc,
        rdb:        rdb,
        localTTL:   5 * time.Minute,
        redisTTL:   30 * time.Minute,
    }
}

// Get 多级缓存查询
func (mlc *MultiLevelCache) Get(ctx context.Context, key string,
    loader func() (string, error)) (string, error) {

    // L1: 本地缓存
    if val, ok := mlc.localCache.Get(key); ok {
        return val, nil
    }

    // L2: Redis缓存
    val, err := mlc.rdb.Get(ctx, key).Result()
    if err == nil && val != "" {
        // 回填L1
        mlc.localCache.Add(key, val)
        return val, nil
    }

    // L3: 数据库
    result, err := loader()
    if err != nil {
        return "", err
    }

    // 回填L2和L1,TTL加随机抖动
    redisTTL := mlc.redisTTL + time.Duration(rand.Intn(300))*time.Second
    mlc.rdb.Set(ctx, key, result, redisTTL)
    mlc.localCache.Add(key, result)

    return result, nil
}

// randInt 辅助函数
func randInt(min, max int) int {
    return min + rand.Intn(max-min)
}

Redis高可用架构配置

数据备份恢复方案中,Redis本身的可用性也需保障。Redis Cluster提供自动分片和故障转移,是生产环境的标准方案。

# redis.conf (哨兵模式配置示例)
port 6379
bind 0.0.0.0
protected-mode yes

# 开启AOF持久化
appendonly yes
appendfsync everysec

# 开启RDB快照
save 900 1
save 300 10
save 60 10000

# 内存淘汰策略
maxmemory 4gb
maxmemory-policy allkeys-lru

# 慢查询日志
slowlog-log-slower-than 10000
slowlog-max-len 128

缓存方案选择决策表

场景 推荐方案 注意事项
恶意查询不存在的数据 布隆过滤器 + 空值缓存 布隆过滤器需在数据写入时同步更新
热点Key过期 分布式锁 + 逻辑过期 锁超时时间要大于DB查询时间
大量Key同时过期 随机TTL + 多级缓存 本地缓存容量不宜过大,避免GC压力
Redis整体宕机 Redis Cluster + 降级策略 降级时返回默认值而非报错
缓存与DB不一致 延迟双删 + 消息队列 双删间隔需评估读耗时

NoSQL选型应用中,Redis因其纯内存操作的特性,QPS可达10万+。但缓存不是万能药——引入缓存的同时必须考虑一致性、可用性和防护策略。缓存防护体系的投入在流量高峰期会体现其价值,DBA和SRE需要定期演练缓存故障场景,确保降级链路可用。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-huan-cun-chuan-tou-yu-xue-beng-fang-hu-bu-long-guo/

(0)
小编小编
上一篇 11小时前
下一篇 11小时前

相关推荐