giftiaのblog

redis 缓存雪崩、穿透、击穿

· giftia

缓存的使用

func GetData(key string) (string, error) {
    // 先查询缓存
    value, err := redisClient.Get(key).Result()
    if err == nil {
        return value, nil
    } else if err == redis.Nil {
        // 缓存失效,从数据库查询
        data, err := db.GetData(key)
        if err != nil {
            return "", err
        }
        // 缓存到redis
        redisClient.Set(key, data, 30*time.Minute)
        return data, nil
    } else {
        return "", err
    }
}

三个问题

缓存击穿

  1. 热key突然过期,大量请求打到数据库
  2. 冷key突然变热key,过期后大量请求打到数据库

缓存雪崩

  1. 大量key集中过期,请求全部打到数据库
  2. redis 服务器宕机,大量请求打到数据库

缓存穿透

  1. 缓存和数据库中都没有,大量请求打到数据库(恶意攻击)(比如id为负或id巨大)

解决方案

缓存击穿

  1. 永不过期
  • 热key永不过期
  • 冷key先定时过期,监控访问量到某个数量时,设置永不过期
  1. 加锁排队
  • 缓存过期后,对于大量请求,使用分布式锁,保证只有一个线程去查询数据库然后重置缓存,其他线程等待从缓存中查询。
func GetData(key string) (string, error) {
    // 先查询缓存
    value, err := redisClient.Get(key).Result()
    if err == nil {
        return value, nil
    } else if err == redis.Nil {
        // 缓存失效,加锁排队
        redisClient.Lock()
        defer redisClient.Unlock()
        // 再次查询缓存(防止上一个抢到锁的线程已经重置了缓存)
        value, err = redisClient.Get(key).Result()
        if err == nil {
            return value, nil
        } else {
            // 缓存失效,从数据库查询
            data, err := db.GetData(key)
            if err != nil {
                return "", err
            }
            // 缓存到redis
            redisClient.Set(key, data, 30*time.Minute)
            return data, nil
        }
    } else {
        return "", err
    }
}

缓存雪崩

  1. 随机过期时间
  • 随机设置缓存过期时间,避免集中过期。
func GetData(key string) (string, error) {
    // 先查询缓存
    value, err := redisClient.Get(key).Result()
    if err == nil {
        return value, nil
    } else if err == redis.Nil {
        // 缓存失效,加锁排队
        redisClient.Lock()
        defer redisClient.Unlock()
        // 再次查询缓存(防止上一个抢到锁的线程已经重置了缓存)
        value, err = redisClient.Get(key).Result()
        if err == nil {
            return value, nil
        } else {
            // 缓存失效,从数据库查询
            data, err := db.GetData(key)
            if err != nil {
                return "", err
            }
            // 缓存到redis, 随机过期时间
            expire := time.Duration(rand.Intn(30*60)) * time.Second
            redisClient.Set(key, data, expire)
            return data, nil
        }
    } else {
        return "", err
    }
}
  1. redis 高可用(解决服务器宕机)
  • 集群、哨兵模式、主从模式、容灾备份

缓存穿透

  1. 参数校验(不能完全杜绝)
  • 对于查询参数,做必要的校验,比如id是否合法,长度是否合法等。
  1. 缓存空值
  • 对于查询不到的数据,设置一个空值到缓存,避免频繁查询数据库。
func GetData(key string) (string, error) {
    // 先查询缓存
    value, err := redisClient.Get(key).Result()
    if err == nil {
        return value, nil
    } else if err == redis.Nil {
        // 缓存失效,加锁排队
        redisClient.Lock()
        defer redisClient.Unlock()
        // 再次查询缓存(防止上一个抢到锁的线程已经重置了缓存)
        value, err = redisClient.Get(key).Result()
        if err == nil {
            return value, nil
        } else {
            // 缓存失效,从数据库查询
            data, err := db.GetData(key)
            if err != nil {
                if err == db.ErrDataNotFound {
                    // 缓存空值
                    redisClient.Set(key, "", 30*time.Minute)
                    return "", nil
                }
                return "", err
            }
            // 缓存到redis, 随机过期时间
            expire := time.Duration(rand.Intn(30*60)) * time.Second
            redisClient.Set(key, data, expire)
            return data, nil
        }
    } else {
        return "", err
    }
}
  1. 布隆过滤器(黑白名单)
  • 对于查询的不存在id,放到布隆过滤器中,黑名单,不通过。
  • 对于查询的存在id,放到布隆过滤器中,白名单,通过。