结论:别手写缓存,优先用ristretto;若必须自研,唯一可控路径是lru.cache加懒检查过期,其余方案如sync.map因无序遍历、不支持访问顺序维护等缺陷,在生产环境极易oom或淘汰失控。
直接上结论:别手写,优先用 ristretto;若必须自研,lru.cache + 懒检查过期 是唯一可控路径,其余方案在生产环境大概率踩坑。
为什么不能用 sync.Map 做 LRU/LFU 缓存
sync.Map 根本不记录访问顺序,Range 遍历无序,也没法原子完成「查 key → 移到链表头」这个 LRU 最小操作单元。实测中缓存项看似存在,但淘汰完全随机,内存只增不减,压测几小时就 OOM。它适合存开关、白名单这类几乎不变的只读数据,但凡带「淘汰」「过期」「访问频次」任一需求,就必须换掉。
lru.Cache + 懒检查过期 是最稳的手动组合
第三方库 github.com/hashicorp/golang-lru/v2 的 lru.Cache 本身不带 TTL,但结构干净、无全局锁、支持并发读写。加过期只需两步:
- Set 时存
value和expireAt time.Time字段(别用 int64 时间戳,易出时区/精度问题) - Get 时先取值,再立刻判断
expireAt.Before(time.Now()),过期就delete并返回未命中 - 绝不自动在 Get 里删 key 后返回
nil, false—— 并发 Get 可能同时触发删除,导致日志刷屏、监控误报
LFU 不是“只比访问次数”,漏掉时间 fallback 就等于没做
标准 LFU 要求:先比访问频次;频次相同时,再比最近一次访问时间(LRU fallback)。否则两个都访问过 1 次的 key,一个刚写入、一个半小时前写入,会随机淘汰,命中率剧烈波动。
- Go 标准库
container/heap不支持复合排序,必须自己实现heap.Interface,在Less方法里嵌套判断 - 只靠
map[key]*heapItem+heap会导致 Get 时无法 O(1) 更新频次和时间 —— heap 不支持按 key 查节点,得遍历,复杂度退化成 O(n) - 别用
sync.Map:它不支持遍历,你连 heap 里所有元素都拿不到,更没法做heap.Fix
ristretto 是目前 Go 生态唯一把 TTL + cost 控制 + 无锁读 + 异步清理做对的库
如果你不想自己封装过期、淘汰、并发三层逻辑,ristretto 就是省心选择。它默认禁用后台清理(Policy: false),但 Set 时传 ristretto.TTL,底层自动处理懒删和驱逐。
- 初始化别设
NumCounters: 1e7,容易让小对象撑爆内存;改用MaxCost: 100 * 1024 * 1024(100MB)更靠谱 - 它不依赖访问时间戳做淘汰,而是基于采样 + cost 估算,对大对象友好,也避免了时间判断带来的锁竞争
- 高频小对象 + 强一致性?别硬套 ristretto —— 改用
groupcache的本地副本模式,它用singleflight.Group防击穿,比纯内存缓存更适合防重复加载
真正难的不是实现某个算法,而是控制淘汰的副作用:GC 压力、锁争用、过期时机不可控、并发删除冲突。这些细节藏在每行 map 操作和每次 time.Now() 调用里,稍不注意,缓存就从加速器变成定时炸弹。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











