Go 缓存策略
Go 服务中的缓存方案,涵盖 sync.Map、本地内存缓存、Redis 集成、Cache-aside 模式与缓存失效策略。
#type / concept
#status / growing
#tech / dev
#resource / go
[!info] related notes
- 所属 MOC: Go 服务工程
- 前置概念: Go Context
- 并列概念: Go 限流, Go 配置管理
Go 缓存策略
一句话定义
Go 缓存策略是在服务中通过本地内存缓存(sync.Map、bigcache、ristretto)或分布式缓存(Redis)减少重复计算和数据库查询,以空间换时间提升性能的工程实践。
核心机制 / 工作原理
sync.Map(并发安全 Map):
var cache sync.Map
// 写入
cache.Store("user:123", userData)
// 读取
if v, ok := cache.Load("user:123"); ok {
user := v.(User)
}
// 读取或写入(原子操作)
v, _ := cache.LoadOrStore("user:123", defaultUser)
适用于读多写少、Key 相对稳定的场景。无 TTL、无淘汰策略、无内存限制,长期运行可能内存泄漏。
bigcache / ristretto(本地内存缓存库):
// bigcache:大容量、低 GC 压力(序列化为 []byte 存储)
cache, _ := bigcache.New(context.Background(), bigcache.Config{
Shards: 1024,
LifeWindow: 10 * time.Minute, // TTL
CleanWindow: 5 * time.Minute, // 清理周期
MaxEntriesInWindow: 1000 * 10 * 60,
MaxEntrySize: 500,
HardMaxCacheSize: 512, // MB
})
cache.Set("key", []byte("value"))
data, _ := cache.Get("key")
// ristretto:基于 LFU/TinyLFU 淘汰,命中率更高
cache, _ := ristretto.NewCache(&ristretto.Config{
NumCounters: 1e7, // 追踪频率的计数器数量
MaxCost: 1 << 30, // 最大内存(字节)
BufferItems: 64,
})
cache.SetWithTTL("key", value, cost, 5*time.Minute)
cache.Wait() // 等待写入缓冲区刷新
if v, found := cache.Get("key"); found {
// 使用缓存值
}
Cache-Aside 模式(最常用):
1. 读请求 -> 先查缓存
2. 缓存命中 -> 直接返回
3. 缓存未命中 -> 查数据库 -> 写入缓存 -> 返回
4. 写请求 -> 更新数据库 -> 删除缓存(非更新缓存)
func GetUser(ctx context.Context, id string) (*User, error) {
// 1. 查缓存
if cached, err := cache.Get("user:" + id); err == nil {
var u User
json.Unmarshal(cached, &u)
return &u, nil
}
// 2. 查数据库
user, err := db.GetUser(ctx, id)
if err != nil {
return nil, err
}
// 3. 写入缓存
data, _ := json.Marshal(user)
cache.SetWithTTL("user:"+id, data, 1, 10*time.Minute)
return user, nil
}
Redis 集成:
import "github.com/redis/go-redis/v9"
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
// 写入(带 TTL)
rdb.Set(ctx, "user:123", jsonData, 10*time.Minute)
// 读取
val, err := rdb.Get(ctx, "user:123").Result()
if err == redis.Nil {
// 缓存未命中
}
最小例子 / 最小场景
// 简单的 TTL 缓存封装
type TTLCache struct {
items map[string]cacheEntry
mu sync.RWMutex
}
type cacheEntry struct {
value []byte
expiresAt time.Time
}
func (c *TTLCache) Get(key string) ([]byte, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.items[key]
if !ok || time.Now().After(entry.expiresAt) {
return nil, false
}
return entry.value, true
}
为什么重要
- 降低延迟:缓存命中可将毫秒级数据库查询降低到微秒级内存读取。
- 减少数据库压力:热点数据缓存后,数据库 QPS 可降低 50%-90%。
- 提升吞吐:缓存层天然支撑高并发,是应对流量尖峰的关键手段。
- 成本控制:减少数据库实例扩容需求,缓存服务器成本远低于数据库。
边界与易混淆点
- 缓存穿透:查询不存在的 Key,每次都穿透到数据库。解决方案:缓存空值(短 TTL)或布隆过滤器。
- 缓存击穿:热点 Key 过期瞬间,大量并发请求同时打到数据库。解决方案:singleflight 合并并发请求。
- 缓存雪崩:大量 Key 同时过期,数据库瞬时压力暴增。解决方案:TTL 加随机偏移。
- 更新策略:Cache-aside 中”删除缓存”优于”更新缓存”——更新缓存在并发场景下容易导致缓存与数据库不一致。
- singleflight 与缓存配合:
确保同一 Key 的并发查询只执行一次数据库查询。var g singleflight.Group v, err, _ := g.Do("user:"+id, func() (interface{}, error) { return db.GetUser(ctx, id) }) - 本地缓存 vs 分布式缓存:单实例用 ristretto/bigcache;多实例共享状态用 Redis。两者可组合:L1 本地 + L2 Redis。