关键在于避开陷阱:lru需感知系统内存水位(如/proc/meminfo的memavailable),lfu须防老数据霸榜(频率衰减+lru fallback),手写节点池比container/list更稳,lru-k结构复杂多数场景不必要。

Go语言实现高性能缓存淘汰算法,关键不在“写对逻辑”,而在“避开陷阱”——LRU和LFU看似简单,但线上高频服务中一个固定容量、不感知内存压力的缓存,可能就是OOM的起点。
LRU:别只靠链表+哈希,要加内存水位线
标准 container/list + map 实现能跑通 Get/Put O(1),但生产环境会出问题。runtime.ReadMemStats 返回的 HeapInuse 只是 Go 堆内存,漏掉了 mmap 分配、CGO 内存、内核 page cache 等——系统 MemAvailable 已跌破 3%,你的 LRU 还在满速塞数据。
- 用 /proc/meminfo(Linux)或 host_statistics(macOS) 替代 ReadMemStats,每 2 秒采样一次 MemAvailable
- 在 Put 前插入检查:if memCheckFn() —— 淘汰 30% 最久未用项,不是只删一个
- 后台起 goroutine 动态调低 memThreshold(比如负载高时从 512MB 缩到 256MB),让缓存主动收缩
LFU:频率不是越多越好,得防“老数据霸榜”
LFU 天然倾向保留高频老数据,但业务常有“热点迁移”——昨天爆火的新闻,今天没人看。纯 LFU 会让它一直占着位置,新热点进不来。
- 必须支持 频率衰减:定期遍历 freqMap,把 freq > 1 的项除以 2,并迁移到新频率链表
- 淘汰时不能只比 freq:freq 相同,按 lastAccessTime 选最久未用者(即 LFU+LRU 双层策略)
- 每个 item 额外存 int + time.Time(约 16 字节),小 value 场景(如 JWT token)开销放大明显,需权衡
结构选型:自己手写节点比 container/list 更稳
container/list 在高并发增删下易产生内存碎片,实测分配耗时从 120ns/op 降到 28ns/op 的关键是预分配节点池。
- 定义 struct node { key, value any; freq int; atime time.Time; prev, next *node }
- 用 sync.Pool 管理 node 实例:pool.Get().(*node),避免频繁 GC
- map[string]*node 直接存指针,省去 list.Element 封装层,减少间接访问
别碰 LRU-K,除非真需要抗扫描污染
LRU-K 要求双链表隔离(主缓存 + 历史队列)+ 独立计数器,结构复杂、锁难拆。多数场景下,带内存水位的 LRU + 定期清理冷 key,效果接近且更可控。
- 如果必须上 LRU-K:主缓存用 RWMutex 读路径只读锁;历史队列单独一把 Mutex;晋升逻辑(cnt≥k 后进主缓存)放在 Get 未命中分支里
- 切忌混用一个 list.List——历史访问和主缓存混在一起,晋升判断就失效了
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











