sync.map不能当主力本地缓存,因其无ttl、不淘汰、无容量控制、无命中统计,也不自动同步redis;仅适用于session id映射等极轻量场景,高频写会导致内存持续上涨与gc压力陡增。

为什么不能直接用 sync.Map 当主力本地缓存
它不是缓存,只是线程安全的哈希表。没 TTL、不淘汰、无容量控制、无命中统计,更不会自动同步到 Redis。你写个 time.Now().Unix() 判断过期,在容器里时钟漂移后,L1 就永远不清理。
常见误用场景:
- 往
sync.Map里塞上万条用户数据,内存持续上涨,GC 压力陡增——它内部 read/dirty map 双结构,频繁Store+Delete会不断复制 dirty map - 包装成带
expireAt字段的 struct + 定时 goroutine 清理——GC 被卡住,且无法响应 key 突增 - 当成 map 的“并发替代品”直接替换旧代码,结果上线后缓存命中率快速下滑
只适合极轻量固定场景:session ID 映射、预热后几十个配置项快照。
选 bigcache 还是 gcache?看你的读写特征
bigcache 更适合微服务真实高并发场景:value 存在预分配字节池,避免堆分配;默认 256 分片锁,写操作互不影响;OnRemove 回调方便打点统计淘汰行为。
gcache 更适合中小规模、需要 LRU/LFU/ARC 多策略、且值类型较轻(如 struct 指针少、不含 slice)的业务。
关键初始化差异:
-
bigcache.NewBigCache必须设MaxEntries和LifeWindow,否则内存无限增长;Shards至少 64(低于 16 会明显抖动) -
gcache.New(1000).LRU().ExpireIn(5 * time.Minute).Build()必须调Build()才能用,否则Get()直接 panic - 两者都不支持直接存 struct 指针——
bigcache存的是[]byte,需提前json.Marshal;gcache存 interface{},但含指针时深拷贝得自己处理
本地缓存必须和 Redis 协同的三个硬约束
多级缓存真正的复杂点不在代码结构,而在时间窗口里的一致性博弈——L1 的 TTL、L2 的 TTL、DB 更新耗时,三者只要差 100ms,就可能暴露脏数据。
- L1 的 TTL 必须 ≤ L2 的剩余 TTL:读取前先调
redis.PTTL(key)获取 Redis 中 key 剩余时间,再动态传给l1.Set(key, val, shortTTL) - 读流程必须走
singleflight.Do防击穿:l1.Get→ miss →singleflight.Do(key, func() { l2.Get() })→ L2 miss → 查 DB →l2.SetEX→ 再l1.Set - 写流程必须严格“删 L1 → 删 L2 → 更新 DB”:绝不能反向,否则中间窗口返回旧 L1 + 新 DB
缓存穿透防护也要两级联动:L2 层对空结果写 redis.SetEX("user:123:empty", "1", 5*time.Minute),L1 同步写一个带 60s TTL 的空标记。
Key 设计和序列化最容易被忽略的细节
本地缓存失效不是靠“删”,而是靠“过期”。所以 key 的语义和序列化方式直接影响一致性边界。
- key 必须带业务版本号:
user:v2:1001,别只拼user:1001——字段变更后旧缓存还在命中,就是这里漏了 - 敏感字段(如手机号)必须先
sha256再拼接,避免缓存 key 泄露业务信息 - 别把结构体 JSON 字符串当 key——序列化顺序不稳定、空格/换行易变,相同逻辑生成不同 key
- 如果用 Redis Cluster,key 中的
{}包裹部分必须一致,比如{user}:1001,否则跨 slot 查询失败
最常被跳过的动作:每次更新缓存前,检查 value 是否含指针或 slice;含的话,bigcache 不自动深拷贝,得自己 json.Marshal 后再存。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











