背景 缓存能提升性能,但也最容易制造隐性 bug。
线上常见问题:
数据库已更新,缓存还是旧值 热点 key 失效瞬间把数据库打穿 多服务写入同一份数据,更新顺序错乱 在 Go 服务里,缓存一致性通常不是“技术选型”问题,而是“写路径设计”问题。
常见策略 Cache Aside 最常见模型:读先查缓存,未命中再查库并回填;写时先写库,再删缓存。
func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) { key := fmt.Sprintf("user:%d", id) if val, ok := s.cache.Get(ctx, key); ok { return decodeUser(val) } user, err := s.repo.FindByID(ctx, id) if err != nil { return nil, err } _ = s.cache.Set(ctx, key, encodeUser(user), 5*time.Minute) return user, nil } func (s *UserService) UpdateUser(ctx context.Context, user *User) error { if err := s.repo.Update(ctx, user); err != nil { return err } key := fmt.Sprintf("user:%d", user.ID) _ = s.cache.Delete(ctx, key) return nil } 这个模型简单、可靠,适合大多数业务系统。
双删策略的取舍 有些场景会用“先删缓存,再写库,延迟再删一次”。
它能降低极端并发下的脏读概率,但不是银弹。更关键的还是:
写操作是否集中在一条服务链路 有没有事件通知机制统一刷新 key 的 TTL 是否合理 避免缓存雪崩 两个实用点:
TTL 加随机抖动 热点 key 做单飞保护 var g singleflight.Group func (s *UserService) GetUserWithSingleflight(ctx context.Context, id int64) (*User, error) { key := fmt.Sprintf("user:%d", id) v, err, _ := g.Do(key, func() (interface{}, error) { return s.GetUser(ctx, id) }) if err != nil { return nil, err } return v.(*User), nil } 总结 缓存一致性最重要的不是某个技巧,而是明确一致性目标:
...