Go 缓存策略

Go 服务中的缓存方案,涵盖 sync.Map、本地内存缓存、Redis 集成、Cache-aside 模式与缓存失效策略。

#type / concept #status / growing #tech / dev #resource / go

[!info] related notes

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 与缓存配合
    var g singleflight.Group
    v, err, _ := g.Do("user:"+id, func() (interface{}, error) {
        return db.GetUser(ctx, id)
    })
    确保同一 Key 的并发查询只执行一次数据库查询。
  • 本地缓存 vs 分布式缓存:单实例用 ristretto/bigcache;多实例共享状态用 Redis。两者可组合:L1 本地 + L2 Redis。
创建于 2026/6/25 更新于 2026/6/25