Golang本地缓存与分布式缓存如何选择_性能与一致性分析

本地缓存适合读多写少、更新不频繁且允许短暂不一致的场景,如用户配置、静态字典;优势是零网络开销、纳秒级延迟、百万级QPS,但存在进程重启丢失、多实例不同步、无法主动失效等问题。

本地缓存适合什么场景?

当数据读多写少、更新不频繁、且允许短暂不一致(比如用户配置、静态字典、开关状态),sync.Mapristretto 这类内存缓存就足够了。它没有网络开销,延迟在纳秒到微秒级,吞吐量轻松过百万 QPS。

但要注意:进程重启后全丢;多实例部署时各节点缓存不同步;无法主动失效——比如你改了数据库里某条商品价格,所有本地缓存不会自动刷新。

  • 适用:http.Handler 内部临时缓存请求上下文、短生命周期的计算结果
  • 慎用:goroutine 频繁写入同一 sync.Map 键——高并发下会退化为锁竞争,不如预分配 map + sync.RWMutex
  • 别踩坑:ristretto 默认不开启 OnEvict 回调,想做缓存穿透防护或日志埋点得手动配

分布式缓存该选 Redis 还是其他?

Redis 是事实标准,但不是万能解。如果你需要强一致性(比如库存扣减)、原子操作(INCRSETNX)、或复杂数据结构(ZSET 做排行榜),那必须上 Redis。但它的网络 RTT、序列化开销、连接池争用,会让 P99 延迟跳到毫秒级。

如果只是做纯读缓存,且能接受最终一致,Redis Cluster 节点扩缩容时会出现短暂 MOVEDASK 错误;而 etcdConsul 更适合元数据类缓存(服务发现、配置中心),它们不支持 LRU 驱逐,也不适合存大 Value。

  • Redis 用连接池:Go 客户端如 github.com/redis/go-redis/v9 必须设置 MinIdleConnsMaxConnAge,否则空闲连接堆积导致 TIME_WAIT 爆满
  • 避免大 Key:GET 一个 10MB 的 JSON 会阻塞 Redis 单线程,也拖慢 Go 的 goroutine;应提前拆分或压缩
  • 不要用 KEYS *:生产环境必须禁用,改用 SCAN 分批处理

本地 + 分布式混合缓存怎么搭才不翻车?

常见模式是「先查本地,未命中再查 Redis,回填本地」,但这个逻辑本身有竞态:两个 goroutine 同时查不到,都会去 Redis 加载,造成击穿和重复写本地缓存。

正确做法是加一层轻量级本地锁(比如 singleflight.Group),让同 key 的并发请求只放行一个去加载,其余等待返回。同时要控制本地缓存 TTL 略短于 Redis,防止本地一直不更新。

var cacheGroup singleflight.Group

func GetItem(id string) (Item, error) { // 先查本地 if item, ok := localCache.Load(id); ok { return item.(Item), nil } // 未命中,用 singleflight 防击穿 v, err, _ := cacheGroup.Do(id, func() (interface{},

error) { item, err := redisClient.Get(ctx, "item:"+id).Result() if err != nil { return nil, err } localCache.Store(id, item) // 回填本地,TTL 设为 redisTTL - 5s return item, nil }) return v.(Item), err }
  • 本地缓存键名和 Redis 键名必须严格一致,否则回填失效
  • singleflight 不处理缓存删除,DEL Redis 后本地仍存在脏数据——需配合发布订阅(如 Redis Pub/Sub)通知其他节点清本地
  • 别把 time.Now().Unix() 当作本地缓存过期依据,Go 的 time.Time 不可比较,要用 time.Since() 判断是否超时

一致性到底能不能兼顾性能?

不能。这是个明确的取舍:你要强一致(比如订单状态变更后立刻可见),就得牺牲性能——用 Redis 事务 + Lua 脚本保证读写原子性,或引入消息队列异步双删;你要高性能,就得接受几秒甚至几分钟的不一致,靠定时任务或监听 binlog 主动刷新缓存。

最容易被忽略的是「缓存雪崩」:大量 key 设置相同过期时间,到期后集体失效,瞬间打垮下游 DB。解决方案不是加随机 offset(治标),而是用「永不过期 + 后台异步更新」策略,或者用 RedisEXPIRE 配合 GETEX 命令实现懒更新。

  • 双删失败无重试机制?必须记录失败日志并走补偿任务,否则缓存永远不一致
  • 本地缓存没设最大容量?ristretto 默认 128MB,但若 key 小 value 大(比如缓存整张用户表),OOM 风险极高
  • json.Marshal 存 struct 到 Redis?注意字段 tag 是否含 omitempty,空值可能被忽略导致反序列化失败