缓存键必须标准化以保障命中率,需排序参数、哈希结构体、剔除动态值、控制长度;优先用sync.map或bigcache;必用singleflight防雪崩;命中率须通过prometheus实时监控。

缓存键(key)必须标准化,否则命中率归零
相同业务逻辑的请求,若因参数顺序、大小写、空格或结构体序列化方式不同生成多个 key,就会导致重复缓存和低命中。这不是“没缓存”,而是“缓存了但用不上”。
- 对查询参数 map 使用
sort.Strings排序键名后再拼接,避免map[string]int{"a":1,"b":2}和{"b":2,"a":1}生成不同 key - 结构体不要直接
json.Marshal作 key——字段顺序不保证,且含空字段、缩进、换行都会破坏一致性;改用sha256.Sum256哈希规范后的字节流 - 避免在 key 中嵌入时间戳、随机数、请求 ID 等动态值,除非该 key 本意就是单次有效
- key 长度控制在 256 字节内,过长会拖慢
sync.Map的 hash 计算和内存占用
别直接用普通 map + mutex,优先选 sync.Map 或 bigcache
普通 map 加 sync.Mutex 在高并发读场景下锁竞争严重,sync.RWMutex 虽缓解但仍存在写饥饿问题。而 sync.Map 底层分片+只读/读写双 map 设计,天生适合读多写少的缓存场景。
- 纯内存缓存且数据量 sync.Map,零依赖、开箱即用、GC 友好
- 缓存项 > 10 万、value 较大(如 JSON 字符串 >1KB)、GC 频繁 → 切到
bigcache,它用分片 + 环形 buffer 绕过 Go GC 扫描 - 需要淘汰策略(如 LRU)→ 用
golang-lru,注意其Cache实例本身不是线程安全的,需外层加sync.RWMutex或用它自带的ARC线程安全变体 - 切忌自己实现带过期的 map:TTL 检查时机难控,易漏删或误删;用
expirable.Cache(来自 golang-lru)或freecache的 TTL 支持更稳妥
singleflight 不是可选项,是防止雪崩的必需组件
当一个热点 key 过期瞬间,十几个 goroutine 同时 Get 失败并触发底层加载,数据库或下游服务可能被瞬时打挂。这不是理论风险,是上线后必现的问题。
- 所有对外部资源(DB、HTTP、RPC)发起的加载逻辑,必须包裹在
singleflight.Group.Do中 - key 必须与缓存 key 语义一致,例如用户详情缓存 key 是
"user:123",那singleflight的 key 也得是它,不能是"load_user_123" - 注意
Do返回的val是 interface{},需显式类型断言;错误要检查err != nil,别只看val是否为 nil - 不要在
Do回调里再调用其他带singleflight的函数,否则可能死锁或嵌套超时
命中率监控不能靠日志,得用 Prometheus 实时指标
靠 log.Printf("hit: %d, miss: %d") 查命中率,等于把仪表盘建在纸面上——你永远不知道它什么时候掉到 40%,也不知道是哪个 key 拖垮了整体。
- 定义两个
prometheus.CounterVec:cache_hits_total{cache="user",driver="syncmap"}和cache_misses_total - 每次
Get成功且未过期,就hits.WithLabelValues(cacheName, driver).Inc();否则misses.Inc() - 在 HTTP handler 或 gRPC interceptor 中统一埋点,避免业务代码到处写指标更新
- 命中率 =
rate(cache_hits_total[1m]) / (rate(cache_hits_total[1m]) + rate(cache_misses_total[1m])),Grafana 里配个阈值告警(如 - 光看命中率不够,还得加
cache_item_count和cache_evictions_total,才能区分是 key 设计差,还是缓存容量真不够
singleflight 的 key 和缓存 key 是否对齐、以及指标是否能真实反映线上行为——这些细节不抠清楚,缓存系统越用越像黑盒。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











