缓存优化
问题
Go 服务中缓存应该怎么设计?如何防止缓存击穿?
答案
缓存层次
本地缓存
// 使用 github.com/dgraph-io/ristretto
import "github.com/dgraph-io/ristretto"
cache, _ := ristretto.NewCache(&ristretto.Config{
NumCounters: 1e7, // 跟踪频率的 key 数
MaxCost: 1 << 30, // 最大内存 1GB
BufferItems: 64,
})
cache.Set("key", value, 1) // cost=1
value, found := cache.Get("key")
// 或用 sync.Map(简单场景)
// 或用 github.com/patrickmn/go-cache(带 TTL)
singleflight 防击穿
import "golang.org/x/sync/singleflight"
var g singleflight.Group
func GetUser(id string) (*User, error) {
// 同一个 key 的并发请求只执行一次
result, err, _ := g.Do("user:"+id, func() (interface{}, error) {
// 查缓存
if user, ok := cache.Get(id); ok {
return user, nil
}
// 缓存未命中,查 DB
user, err := db.FindUser(id)
if err != nil {
return nil, err
}
cache.Set(id, user, time.Minute*5)
return user, nil
})
if err != nil {
return nil, err
}
return result.(*User), nil
}
singleflight 原理
多个 goroutine 同时请求同一个 key 时,只有第一个去查 DB,其他的等待结果共享。适合防止缓存过期瞬间的并发穿透。
缓存一致性
| 策略 | 流程 | 一致性 |
|---|---|---|
| Cache Aside | 写 DB → 删缓存 | 最终一致 |
| Write Through | 写缓存 → 写 DB | 强一致 |
| Write Behind | 写缓存 → 异步 DB | 弱一致 |
常见面试问题
Q1: 缓存穿透、击穿、雪崩的区别和解决方案?
答案:
| 问题 | 原因 | 解决 |
|---|---|---|
| 穿透 | 查不存在的 key | 布隆过滤器、缓存空值 |
| 击穿 | 热点 key 过期 | singleflight、互斥锁 |
| 雪崩 | 大量 key 同时过期 | 随机 TTL、多级缓存 |
Q2: singleflight 和 sync.Once 的区别?
答案:
sync.Once:全局只执行一次,结果永久缓存singleflight:同时刻只执行一次,下次请求还会执行
singleflight 适合做请求级去重,sync.Once 适合做初始化。