
Go缓存系统项目复盘从单机到分布式缓存的架构升级实战经验一、单机缓存的三个阶段一个Go API服务从无缓存到分布式缓存经历了三个自然阶段。这不是设计出来的架构而是被业务压力推着走的演进。阶段一无缓存 → 内存缓存API请求量增长让数据库成为瓶颈。引入sync.Map做进程内缓存如用户信息缓存。命中率约60%API延迟从120ms降到50ms。问题多实例部署时缓存不一致——实例A更新用户信息实例B的缓存仍是旧的。阶段二内存缓存 → Redis为解决多实例一致性问题切换为Redis集中缓存。问题解决了但Redis成了新的瓶颈——所有读请求都经过Redis高峰期Redis CPU打到80%。阶段三Redis → 两级缓存在应用进程内加L1缓存本地Redis作为L2共享。热点数据在L1命中不穿透到Redis。Redis CPU从80%降到30%。二、两级缓存的核心实现L1进程内缓存go-cache 过期控制import github.com/patrickmn/go-cache type TwoLevelCache struct { l1 *cache.Cache l2 *redis.Client pubsub *redis.PubSub // 缓存失效通知 } func NewTwoLevelCache() *TwoLevelCache { return TwoLevelCache{ l1: cache.New(30*time.Second, 60*time.Second), // 默认TTL 30s } } func (c *TwoLevelCache) Get(key string, target interface{}) error { // 1. 查L1 if val, found : c.l1.Get(key); found { // JSON反序列化 json.Unmarshal(val.([]byte), target) return nil } // 2. L1 miss → 查L2 val, err : c.l2.Get(ctx, key).Bytes() if err nil { // 命中L2回写L1 c.l1.Set(key, val, 30*time.Second) json.Unmarshal(val, target) return nil } if err ! redis.Nil { return err } // 3. L2 miss → 查DB return ErrCacheMiss }缓存一致性的主动失效方案// 数据更新时——先更新DB再失效缓存 func (s *UserService) UpdateUser(ctx context.Context, user *User) error { // 1. 更新数据库 if err : s.db.Update(ctx, user); err ! nil { return err } // 2. 失效Redis缓存 cacheKey : fmt.Sprintf(user:%d, user.ID) s.cache.l2.Del(ctx, cacheKey) // 3. 发布失效通知——让其他实例也清除L1 s.cache.l2.Publish(ctx, cache:invalidation, cacheKey) return nil } // 监听失效通知 func (c *TwoLevelCache) listenInvalidation() { ch : c.pubsub.Channel() for msg : range ch { cacheKey : msg.Payload c.l1.Delete(cacheKey) // 清除本实例的L1缓存 } }三、缓存问题的三个典型场景缓存穿透查不存在的数据攻击者请求不存在的用户ID每次穿透L1→L2→DB。解决方案——布隆过滤器或缓存空值func (c *TwoLevelCache) GetOrLoad(key string, loader func() (interface{}, error)) (interface{}, error) { // ... L1/L2检查 ... // 查DB data, err : loader() if err ErrNotFound { // 缓存空值——防止穿透但TTL短 c.l1.Set(key, nil, 5*time.Second) c.l2.Set(ctx, key, NULL, 10*time.Second) return nil, ErrNotFound } // 正常回写 c.l1.Set(key, data, 30*time.Second) c.l2.Set(ctx, key, data, 5*time.Minute) return data, nil }缓存击穿热点数据过期瞬间某热点用户的缓存过期时同一时刻100个请求并发查DB。解决——互斥锁func (c *TwoLevelCache) GetWithMutex(key string, loader func() (interface{}, error)) (interface{}, error) { // L1/L2检查... // 获取分布式锁 mutexKey : mutex: key locked, _ : c.l2.SetNX(ctx, mutexKey, 1, 10*time.Second).Result() if locked { // 获得锁——负责加载 defer c.l2.Del(ctx, mutexKey) data, err : loader() if err nil { c.l1.Set(key, data, 30*time.Second) c.l2.Set(ctx, key, data, 5*time.Minute) } return data, err } // 未获得锁——等待后重试 time.Sleep(100 * time.Millisecond) return c.Get(key, nil) }缓存雪崩大量缓存同时过期给每个key的TTL加随机偏移func randomTTL(base time.Duration) time.Duration { jitter : time.Duration(rand.Int63n(int64(base) / 5)) // ±20% return base jitter }四、实际数据指标无缓存单级Redis两级缓存API延迟(P50)120ms8ms2msAPI延迟(P99)500ms45ms12msDB QPS2000600100Redis CPU—80%30%缓存命中率—65%95%两级缓存将数据库查询量降低了95%2000→100 QPSRedis负载降低62%80%→30% CPU。五、总结缓存系统演进的三个阶段是自然路径单机内存缓存 → 解决延迟问题但引入多实例一致性挑战Redis集中缓存 → 解决一致性但引入Redis瓶颈两级缓存 → L1做热点保护L2做共享存储命中率95%核心经验主动失效更新时删除缓存是处理一致性的最简单方案缓存空值短TTL防止缓存穿透分布式锁SET NX防止缓存击穿TTL加随机偏移防止缓存雪崩L1缓存TTL短30sL2缓存TTL长5min——平衡一致性和性能最大的教训缓存不是加一层就完了。每一级缓存的引入都伴随着一致性和失效的新问题。两级缓存已经足够复杂4个并发控制场景穿透、击穿、雪崩、一致性在没有明确的性能压力时不要引入第三级缓存。