那天凌晨两点,告警群炸了:数据库 CPU 99%,接口大面积超时。复盘下来,是三个经典问题的「组合拳」。

先分清三个概念

问题 触发 解法
穿透 查不存在的 key 空值缓存 / 布隆过滤器
击穿 热点 key 过期瞬间 互斥锁 / 逻辑过期
雪崩 大量 key 同时过期 过期时间加随机抖动

我们那次,击穿叠了雪崩:一批热点商品缓存是同一时刻批量写入的,TTL 也相同,到点一起失效,瞬时请求全压到库上。

热点 key 的互斥重建

func getWithLock(key string) (string, error) {
    if v, ok := cache.Get(key); ok {
        return v, nil
    }
    // 只允许一个 goroutine 回源
    if !mutex.TryLock(key) {
        time.Sleep(20 * time.Millisecond)
        return getWithLock(key) // 简单重试
    }
    defer mutex.Unlock(key)

    v := loadFromDB(key)
    cache.Set(key, v, ttlWithJitter())
    return v, nil
}

雪崩的真正解药

不是更复杂的锁,而是给 TTL 加抖动

func ttlWithJitter() time.Duration {
    base := 30 * time.Minute
    jitter := time.Duration(rand.Intn(600)) * time.Second
    return base + jitter
}

写完后那一周,同类告警再没出现过。缓存这东西,八成的问题都出在「时间」上。